From cc116fc440ab9d56a1b91f314c0329d0bcbdc6fc Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 22 Sep 2026 00:29:23 +0300 Subject: [PATCH 01/23] feat(core/app,core/resources): redesign Home screen and simplify onboarding welcome --- .../rv2ide/activities/OnboardingActivity.kt | 3 - .../com/tom/rv2ide/fragments/MainFragment.kt | 214 +++++++++++------- .../main/res/layout-land/fragment_main.xml | 172 ++++++++++---- .../res/layout/bottomsheet_project_list.xml | 9 +- .../app/src/main/res/layout/fragment_main.xml | 188 +++++++++++---- core/app/src/main/res/layout/item_project.xml | 15 +- .../res/layout/layout_main_action_item.xml | 27 ++- .../resources/src/main/res/values/strings.xml | 3 + 8 files changed, 422 insertions(+), 209 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/activities/OnboardingActivity.kt b/core/app/src/main/java/com/tom/rv2ide/activities/OnboardingActivity.kt index 6bfa490b1c..3642a7e750 100644 --- a/core/app/src/main/java/com/tom/rv2ide/activities/OnboardingActivity.kt +++ b/core/app/src/main/java/com/tom/rv2ide/activities/OnboardingActivity.kt @@ -32,7 +32,6 @@ import com.tom.rv2ide.R import com.tom.rv2ide.R.string import com.tom.rv2ide.app.configuration.IDEBuildConfigProvider import com.tom.rv2ide.app.configuration.IJdkDistributionProvider -import com.tom.rv2ide.fragments.onboarding.GreetingFragment import com.tom.rv2ide.fragments.onboarding.IdeSetupConfigurationFragment import com.tom.rv2ide.fragments.onboarding.OnboardingInfoFragment import com.tom.rv2ide.fragments.onboarding.PermissionsFragment @@ -86,8 +85,6 @@ class OnboardingActivity : AppIntro2() { isIndicatorEnabled = true isWizardMode = true - addSlide(GreetingFragment()) - if (!PackageUtils.isCurrentUserThePrimaryUser(this)) { val errorMessage = getString( diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt index 9604f1f0be..9a82c7bf02 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt @@ -106,60 +106,80 @@ class MainFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val actions = - MainScreenAction.all().also { actions -> - val onClick = { action: MainScreenAction, _: View -> - when (action.id) { - MainScreenAction.ACTION_CREATE_PROJECT -> showCreateProject() - MainScreenAction.ACTION_OPEN_PROJECT -> showProjectsBottomSheet() - MainScreenAction.ACTION_CLONE_REPO -> showGitCloneBottomSheet() - MainScreenAction.ACTION_OPEN_TERMINAL -> - startActivity(Intent(requireActivity(), TerminalActivity::class.java)) - - MainScreenAction.ACTION_PREFERENCES -> gotoPreferences() - MainScreenAction.ACTION_DONATE -> { - startActivity(Intent(requireActivity(), IDEConfigurations::class.java)) - } - MainScreenAction.ACTION_DOCS -> BaseApplication.getBaseInstance().openDocs() - } - } + val actions = MainScreenAction.all() + val onClick = { action: MainScreenAction, _: View -> + when (action.id) { + MainScreenAction.ACTION_CREATE_PROJECT -> showCreateProject() + MainScreenAction.ACTION_OPEN_PROJECT -> showProjectsBottomSheet() + MainScreenAction.ACTION_CLONE_REPO -> showGitCloneBottomSheet() + MainScreenAction.ACTION_OPEN_TERMINAL -> + startActivity(Intent(requireActivity(), TerminalActivity::class.java)) + + MainScreenAction.ACTION_PREFERENCES -> gotoPreferences() + MainScreenAction.ACTION_DONATE -> { + startActivity(Intent(requireActivity(), IDEConfigurations::class.java)) + } + MainScreenAction.ACTION_DOCS -> BaseApplication.getBaseInstance().openDocs() + } + } + + actions.forEach { action -> + action.onClick = onClick - actions.forEach { action -> - action.onClick = onClick - - if (action.id == MainScreenAction.ACTION_OPEN_TERMINAL) { - action.onLongClick = { _: MainScreenAction, _: View -> - val intent = - Intent(requireActivity(), TerminalActivity::class.java).apply { - putExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, true) - } - startActivity(intent) - true + if (action.id == MainScreenAction.ACTION_OPEN_TERMINAL) { + action.onLongClick = { _: MainScreenAction, _: View -> + val intent = + Intent(requireActivity(), TerminalActivity::class.java).apply { + putExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, true) } - } - } + startActivity(intent) + true } + } + } - binding!!.actions.adapter = MainActionsListAdapter(actions) - } + val createAction = actions.first { it.id == MainScreenAction.ACTION_CREATE_PROJECT } + binding!!.actionCreateProject.setOnClickListener { + createAction.onClick?.invoke(createAction, it) + } - override fun onDestroyView() { - super.onDestroyView() - binding = null + binding!!.actions.adapter = + MainActionsListAdapter( + actions.filter { + it.id == MainScreenAction.ACTION_OPEN_PROJECT || + it.id == MainScreenAction.ACTION_CLONE_REPO || + it.id == MainScreenAction.ACTION_OPEN_TERMINAL + }) + binding!!.moreActions.adapter = + MainActionsListAdapter( + actions.filter { + it.id == MainScreenAction.ACTION_PREFERENCES || + it.id == MainScreenAction.ACTION_DONATE || + it.id == MainScreenAction.ACTION_DOCS + }) + binding!!.btnViewAllProjects.setOnClickListener { showProjectsBottomSheet() } + + loadRecentProjects() } - private fun showProjectsBottomSheet() { - val bottomSheet = BottomSheetDialog(requireContext()) - val sheetView = layoutInflater.inflate(R.layout.bottomsheet_project_list, null) - - val recyclerView = sheetView.findViewById(R.id.projectsRecyclerView) - val emptyText = sheetView.findViewById(R.id.emptyText) - val btnBrowse = - sheetView.findViewById(R.id.btnBrowse) - val searchEditText = sheetView.findViewById(R.id.searchEditText) + override fun onResume() { + super.onResume() + loadRecentProjects() + } - recyclerView.layoutManager = LinearLayoutManager(requireContext()) + private fun loadRecentProjects() { + val binding = binding ?: return + val recent = collectProjectDirs().take(3) + if (recent.isEmpty()) { + binding.recentSection.visibility = View.GONE + return + } + binding.recentSection.visibility = View.VISIBLE + binding.recentList.layoutManager = LinearLayoutManager(requireContext()) + binding.recentList.adapter = RecentProjectsAdapter(recent, onProjectClick = { openProject(it) }) + } + private fun collectProjectDirs(): List { val projectDirProjects = GeneralFileUtils.listDirsInDirectory(Environment.PROJECTS_DIR).filter { isValidAndroidProject(it) @@ -178,16 +198,35 @@ class MainFragment : BaseFragment() { projectDirProjects.forEach { file -> allProjectsMap[file.absolutePath] = file } - val projectDirs = - allProjectsMap.values - .toList() - .sortedWith( - compareBy { project -> - val recentIndex = recentProjectPaths.indexOf(project.absolutePath) - if (recentIndex >= 0) recentIndex else Int.MAX_VALUE - } - .thenByDescending { it.lastModified() } - ) + return allProjectsMap.values + .toList() + .sortedWith( + compareBy { project -> + val recentIndex = recentProjectPaths.indexOf(project.absolutePath) + if (recentIndex >= 0) recentIndex else Int.MAX_VALUE + } + .thenByDescending { it.lastModified() } + ) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + private fun showProjectsBottomSheet() { + val bottomSheet = BottomSheetDialog(requireContext()) + val sheetView = layoutInflater.inflate(R.layout.bottomsheet_project_list, null) + + val recyclerView = sheetView.findViewById(R.id.projectsRecyclerView) + val emptyText = sheetView.findViewById(R.id.emptyText) + val btnBrowse = + sheetView.findViewById(R.id.btnBrowse) + val searchEditText = sheetView.findViewById(R.id.searchEditText) + + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + + val projectDirs = collectProjectDirs() var adapter: ProjectsListAdapter? = null @@ -207,38 +246,7 @@ class MainFragment : BaseFragment() { }, onProjectLongClick = { project -> showProjectOptionsDialog(project) { - val updatedProjectDirProjects = - GeneralFileUtils.listDirsInDirectory(Environment.PROJECTS_DIR).filter { - isValidAndroidProject(it) - } - val updatedRecentProjectPaths = - WizardPreferences.getRecentProjects(requireContext()) - val updatedRecentProjectFiles = - updatedRecentProjectPaths.mapNotNull { path -> - val file = File(path) - if (file.exists() && file.isDirectory && isValidAndroidProject(file)) file - else null - } - - val updatedAllProjectsMap = mutableMapOf() - updatedRecentProjectFiles.forEach { file -> - updatedAllProjectsMap[file.absolutePath] = file - } - updatedProjectDirProjects.forEach { file -> - updatedAllProjectsMap[file.absolutePath] = file - } - - val updatedDirs = - updatedAllProjectsMap.values - .toList() - .sortedWith( - compareBy { proj -> - val recentIndex = - updatedRecentProjectPaths.indexOf(proj.absolutePath) - if (recentIndex >= 0) recentIndex else Int.MAX_VALUE - } - .thenByDescending { it.lastModified() } - ) + val updatedDirs = collectProjectDirs() adapter?.updateProjects(updatedDirs) @@ -770,6 +778,38 @@ class MainFragment : BaseFragment() { } } + private class RecentProjectsAdapter( + private val projects: List, + private val onProjectClick: (File) -> Unit, + ) : RecyclerView.Adapter() { + + inner class RecentViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val projectName: TextView = view.findViewById(R.id.projectName) + val projectPath: TextView = view.findViewById(R.id.projectPath) + val recentBadge: TextView = view.findViewById(R.id.recentBadge) + val root: View = view + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_project, parent, false) + return RecentViewHolder(view) + } + + override fun onBindViewHolder(holder: RecentViewHolder, position: Int) { + val project = projects[position] + holder.projectName.text = project.name + holder.projectPath.text = project.absolutePath + + val recentRank = + WizardPreferences.getRecentProjectRank(holder.root.context, project.absolutePath) + holder.recentBadge.visibility = if (recentRank in 0..2) View.VISIBLE else View.GONE + + holder.root.setOnClickListener { onProjectClick(project) } + } + + override fun getItemCount() = projects.size + } + private class ProjectsListAdapter( private var projects: List, private val onProjectClick: (File) -> Unit, diff --git a/core/app/src/main/res/layout-land/fragment_main.xml b/core/app/src/main/res/layout-land/fragment_main.xml index 5bbadbf234..32a31dac9d 100644 --- a/core/app/src/main/res/layout-land/fragment_main.xml +++ b/core/app/src/main/res/layout-land/fragment_main.xml @@ -17,77 +17,155 @@ ~ along with AndroidIDE. If not, see . --> - + + android:baselineAligned="false" + android:orientation="horizontal"> - + + + android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.TitleLarge" + android:textStyle="bold" /> - - - - - - + android:textColor="?attr/colorOnSurfaceVariant" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/app/src/main/res/layout/bottomsheet_project_list.xml b/core/app/src/main/res/layout/bottomsheet_project_list.xml index cc1dbeec68..01c5060fe1 100644 --- a/core/app/src/main/res/layout/bottomsheet_project_list.xml +++ b/core/app/src/main/res/layout/bottomsheet_project_list.xml @@ -19,17 +19,18 @@ android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/open_project" - android:textSize="20sp" - android:textStyle="bold" /> + android:textAppearance="@style/TextAppearance.Material3.TitleLarge" + android:textColor="?attr/colorOnSurface" /> @@ -69,6 +70,8 @@ android:layout_height="wrap_content" android:text="@string/no_projects_found" android:textAlignment="center" + android:textAppearance="@style/TextAppearance.Material3.BodyMedium" + android:textColor="?attr/colorOnSurfaceVariant" android:padding="32dp" android:visibility="gone" /> diff --git a/core/app/src/main/res/layout/fragment_main.xml b/core/app/src/main/res/layout/fragment_main.xml index 8ce92b4f3c..007cad8bec 100644 --- a/core/app/src/main/res/layout/fragment_main.xml +++ b/core/app/src/main/res/layout/fragment_main.xml @@ -1,59 +1,149 @@ - - - - - - - - - - - \ No newline at end of file + android:orientation="vertical" + android:padding="20dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/app/src/main/res/layout/item_project.xml b/core/app/src/main/res/layout/item_project.xml index 8d9b7fd491..6696a38d95 100644 --- a/core/app/src/main/res/layout/item_project.xml +++ b/core/app/src/main/res/layout/item_project.xml @@ -3,7 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" + android:layout_marginVertical="4dp" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground" app:cardCornerRadius="16dp" app:strokeWidth="0dp" app:cardBackgroundColor="?attr/colorSurfaceContainerHigh" @@ -26,7 +29,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:textSize="16sp" + android:textAppearance="@style/TextAppearance.Material3.TitleMedium" android:textStyle="bold" android:text="Project Name" /> @@ -35,7 +38,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/recent_badge" - android:textSize="10sp" + android:textAppearance="@style/TextAppearance.Material3.LabelSmall" android:textColor="?attr/colorPrimary" android:background="?attr/colorPrimaryContainer" android:paddingHorizontal="8dp" @@ -49,8 +52,8 @@ android:id="@+id/projectPath" android:layout_width="match_parent" android:layout_height="wrap_content" - android:textSize="12sp" - android:textColor="?android:textColorSecondary" + android:textAppearance="@style/TextAppearance.Material3.BodySmall" + android:textColor="?attr/colorOnSurfaceVariant" android:layout_marginTop="4dp" android:text="/path/to/project" android:ellipsize="middle" @@ -58,4 +61,4 @@ - \ No newline at end of file + diff --git a/core/app/src/main/res/layout/layout_main_action_item.xml b/core/app/src/main/res/layout/layout_main_action_item.xml index 8f8983d0cf..e314b6281e 100644 --- a/core/app/src/main/res/layout/layout_main_action_item.xml +++ b/core/app/src/main/res/layout/layout_main_action_item.xml @@ -5,30 +5,29 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_marginVertical="4dp" + app:cardCornerRadius="16dp" app:cardElevation="0dp" - app:cardCornerRadius="8dp" - app:strokeWidth="0dp" - app:cardBackgroundColor="?attr/colorSurfaceContainerLow"> + app:strokeColor="?attr/colorOutlineVariant" + app:strokeWidth="1dp" + app:cardBackgroundColor="?attr/colorSurfaceContainer"> - - + diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index b56eec7f6d..40b6d81f16 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -194,6 +194,9 @@ No projects match your search. Browse Other Location Recent + Recent projects + View all + More options Delete Project? Are you sure you want to delete \"%1$s\"?\n\nThis will permanently delete all files in this project. This action cannot be undone. From 5511d47e6a9d33663eb23c856d4245577ca15edc Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 22 Sep 2026 00:49:24 +0300 Subject: [PATCH 02/23] ci: fix stale absolute paths in missing-dirs workaround --- .github/workflows/asm_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/asm_build.yml b/.github/workflows/asm_build.yml index ee2efda313..e22190f9cc 100644 --- a/.github/workflows/asm_build.yml +++ b/.github/workflows/asm_build.yml @@ -60,7 +60,7 @@ jobs: distribution: 'adopt' - name: Generating missing dirs - run: mkdir -p /home/runner/work/android-code-studio/android-code-studio/core/app/build/intermediates/external_file_lib_dex_archives/debug/desugarDebugFileDependencies && mkdir -p /home/runner/work/android-code-studio/android-code-studio/core/app/build/intermediates/l8_art_profile/release/l8DexDesugarLibRelease + run: mkdir -p core/app/build/intermediates/external_file_lib_dex_archives/debug/desugarDebugFileDependencies && mkdir -p core/app/build/intermediates/l8_art_profile/release/l8DexDesugarLibRelease - name: Change Gradle wrapper permissions run: chmod +x ./gradlew From 4929dcfd20adaae1e1f67f2e227e32daf066fde6 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 22 Sep 2026 21:31:51 +0300 Subject: [PATCH 03/23] fix(core/app): use standard debug keystore for Debug builds --- core/app/build.gradle.kts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/app/build.gradle.kts b/core/app/build.gradle.kts index be200a7f62..8a6c764916 100755 --- a/core/app/build.gradle.kts +++ b/core/app/build.gradle.kts @@ -97,7 +97,10 @@ android { buildTypes { debug { - signingConfig = signingConfigs.getByName("custom") + // Standard Android debug keystore (auto-generated by AGP). + // Debug builds must not require the release keystore credentials, + // so CI forks without signing secrets can still assembleDebug. + signingConfig = signingConfigs.getByName("debug") } release { @@ -302,3 +305,13 @@ dependencies { compileOnly(projects.tooling.impl) } + +// SigningConfigPlugin (applied to all Android modules via the root build script's +// afterEvaluate hook) assigns its own signing config to every build type, which would +// override the debug assignment above. Re-assert the standard debug keystore here: +// this block runs after the root script's afterEvaluate hook. +afterEvaluate { + extensions.getByType().run { + buildTypes.getByName("debug").signingConfig = signingConfigs.getByName("debug") + } +} From 13ce1c8e399b3c39c0a4dc51b97ef9a65bc66e86 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 22 Sep 2026 23:21:56 +0300 Subject: [PATCH 04/23] feat(core/app): polish home screen layout --- .../rv2ide/adapters/MainActionsListAdapter.kt | 26 +++++-- .../com/tom/rv2ide/fragments/MainFragment.kt | 25 ++++-- .../main/res/layout-land/fragment_main.xml | 24 +++--- .../app/src/main/res/layout/fragment_main.xml | 27 +++---- core/app/src/main/res/layout/item_project.xml | 76 ++++++++++++------- .../layout/layout_main_action_grid_item.xml | 40 ++++++++++ .../res/layout/layout_main_action_item.xml | 26 ++++--- 7 files changed, 169 insertions(+), 75 deletions(-) create mode 100644 core/app/src/main/res/layout/layout_main_action_grid_item.xml diff --git a/core/app/src/main/java/com/tom/rv2ide/adapters/MainActionsListAdapter.kt b/core/app/src/main/java/com/tom/rv2ide/adapters/MainActionsListAdapter.kt index 2342b750d1..1bd6919d55 100644 --- a/core/app/src/main/java/com/tom/rv2ide/adapters/MainActionsListAdapter.kt +++ b/core/app/src/main/java/com/tom/rv2ide/adapters/MainActionsListAdapter.kt @@ -18,24 +18,35 @@ package com.tom.rv2ide.adapters import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import androidx.annotation.LayoutRes import androidx.recyclerview.widget.RecyclerView -import com.tom.rv2ide.databinding.LayoutMainActionItemBinding +import com.google.android.material.button.MaterialButton +import com.tom.rv2ide.R import com.tom.rv2ide.models.MainScreenAction /** * Adapter for the actions available on the main screen. * + * @param itemLayoutRes item layout to inflate. Any layout with a + * [MaterialButton] with ID `R.id.actionButton` works (row or grid tile). + * * @author Akash Yadav */ class MainActionsListAdapter @JvmOverloads -constructor(val actions: List = emptyList()) : - RecyclerView.Adapter() { - class VH(val binding: LayoutMainActionItemBinding) : RecyclerView.ViewHolder(binding.root) +constructor( + val actions: List = emptyList(), + @LayoutRes private val itemLayoutRes: Int = R.layout.layout_main_action_item, +) : RecyclerView.Adapter() { + class VH(val root: View, val actionButton: MaterialButton) : + RecyclerView.ViewHolder(root) - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - VH(LayoutMainActionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val view = LayoutInflater.from(parent.context).inflate(itemLayoutRes, parent, false) + return VH(view, view.findViewById(R.id.actionButton)) + } override fun getItemCount(): Int = actions.size @@ -43,9 +54,8 @@ constructor(val actions: List = emptyList()) : override fun onBindViewHolder(holder: VH, position: Int) { val action = getAction(index = position) - val binding = holder.binding - binding.actionButton.apply { + holder.actionButton.apply { setText(action.text) setIconResource(action.icon) setOnClickListener { action.onClick?.invoke(action, it) } diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt index 9a82c7bf02..67390662b6 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog @@ -143,13 +144,27 @@ class MainFragment : BaseFragment() { createAction.onClick?.invoke(createAction, it) } + val primaryActions = + actions.filter { + it.id == MainScreenAction.ACTION_OPEN_PROJECT || + it.id == MainScreenAction.ACTION_CLONE_REPO || + it.id == MainScreenAction.ACTION_OPEN_TERMINAL + } binding!!.actions.adapter = MainActionsListAdapter( - actions.filter { - it.id == MainScreenAction.ACTION_OPEN_PROJECT || - it.id == MainScreenAction.ACTION_CLONE_REPO || - it.id == MainScreenAction.ACTION_OPEN_TERMINAL - }) + primaryActions, + R.layout.layout_main_action_grid_item, + ) + // Open + Clone share a row; Terminal spans the full width below them. + val gridLayoutManager = GridLayoutManager(requireContext(), 2) + gridLayoutManager.spanSizeLookup = + object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (primaryActions[position].id == MainScreenAction.ACTION_OPEN_TERMINAL) 2 + else 1 + } + } + binding!!.actions.layoutManager = gridLayoutManager binding!!.moreActions.adapter = MainActionsListAdapter( actions.filter { diff --git a/core/app/src/main/res/layout-land/fragment_main.xml b/core/app/src/main/res/layout-land/fragment_main.xml index 32a31dac9d..2dc614b410 100644 --- a/core/app/src/main/res/layout-land/fragment_main.xml +++ b/core/app/src/main/res/layout-land/fragment_main.xml @@ -39,8 +39,8 @@ @@ -59,7 +59,7 @@ android:id="@+id/greetingText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="4dp" + android:layout_marginTop="2dp" android:text="@string/msg_create_new_project_greeting" android:textAlignment="center" android:textAppearance="@style/TextAppearance.Material3.BodyMedium" @@ -78,7 +78,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="20dp"> + android:paddingHorizontal="16dp" + android:paddingTop="16dp" + android:paddingBottom="24dp"> - + + tools:listitem="@layout/layout_main_action_grid_item" /> @@ -150,7 +152,7 @@ @@ -159,7 +161,7 @@ android:id="@+id/moreActions" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="8dp" + android:layout_marginTop="4dp" android:nestedScrollingEnabled="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/layout_main_action_item" /> diff --git a/core/app/src/main/res/layout/fragment_main.xml b/core/app/src/main/res/layout/fragment_main.xml index 007cad8bec..f022f7e199 100644 --- a/core/app/src/main/res/layout/fragment_main.xml +++ b/core/app/src/main/res/layout/fragment_main.xml @@ -14,7 +14,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="20dp"> + android:paddingHorizontal="16dp" + android:paddingTop="16dp" + android:paddingBottom="24dp"> @@ -34,7 +36,7 @@ @@ -50,7 +52,6 @@ android:id="@+id/greetingText" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="2dp" android:text="@string/msg_create_new_project_greeting" android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textColor="?attr/colorOnSurfaceVariant" /> @@ -65,29 +66,29 @@ style="@style/Widget.Material3.Button" android:layout_width="match_parent" android:layout_height="56dp" - android:layout_marginTop="20dp" + android:layout_marginTop="16dp" android:text="@string/new_project" app:cornerRadius="16dp" app:icon="@drawable/ic_add" app:iconGravity="textStart" app:iconPadding="12dp" /> - + + tools:listitem="@layout/layout_main_action_grid_item" /> @@ -130,7 +131,7 @@ @@ -139,7 +140,7 @@ android:id="@+id/moreActions" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="8dp" + android:layout_marginTop="4dp" android:nestedScrollingEnabled="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/layout_main_action_item" /> diff --git a/core/app/src/main/res/layout/item_project.xml b/core/app/src/main/res/layout/item_project.xml index 6696a38d95..d2183d51ae 100644 --- a/core/app/src/main/res/layout/item_project.xml +++ b/core/app/src/main/res/layout/item_project.xml @@ -1,6 +1,7 @@ + android:orientation="horizontal" + android:gravity="center_vertical" + android:padding="12dp"> + + + + + + + android:layout_weight="1" + android:layout_marginStart="12dp" + android:orientation="vertical"> + android:ellipsize="end" + android:singleLine="true" + tools:text="MyProject" /> + android:textAppearance="@style/TextAppearance.Material3.BodySmall" + android:textColor="?attr/colorOnSurfaceVariant" + android:layout_marginTop="2dp" + android:ellipsize="middle" + android:singleLine="true" + tools:text="/storage/emulated/0/AndroidIDEProjects/MyProject" /> + android:text="@string/recent_badge" + android:textAppearance="@style/TextAppearance.Material3.LabelSmall" + android:textColor="?attr/colorPrimary" + android:background="?attr/colorPrimaryContainer" + android:paddingHorizontal="8dp" + android:paddingVertical="4dp" + android:layout_marginStart="8dp" + android:visibility="gone" + tools:visibility="visible" /> diff --git a/core/app/src/main/res/layout/layout_main_action_grid_item.xml b/core/app/src/main/res/layout/layout_main_action_grid_item.xml new file mode 100644 index 0000000000..a3dd940601 --- /dev/null +++ b/core/app/src/main/res/layout/layout_main_action_grid_item.xml @@ -0,0 +1,40 @@ + + + + + + + diff --git a/core/app/src/main/res/layout/layout_main_action_item.xml b/core/app/src/main/res/layout/layout_main_action_item.xml index e314b6281e..7f9ebffa75 100644 --- a/core/app/src/main/res/layout/layout_main_action_item.xml +++ b/core/app/src/main/res/layout/layout_main_action_item.xml @@ -1,33 +1,35 @@ + + app:strokeWidth="0dp" + app:cardBackgroundColor="?attr/colorSurfaceContainerLow"> + app:iconSize="20dp" + app:iconTint="?attr/colorOnSurfaceVariant" + app:rippleColor="?attr/colorSecondaryContainer" + tools:icon="@drawable/ic_settings" + tools:text="@string/msg_preferences" /> From 06176ebd2618aba187934df09a7f4568fe799c0d Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 23 Sep 2026 00:09:46 +0300 Subject: [PATCH 05/23] fix(core/app): point in-app updater at ACS Zen fork metadata --- .../com/tom/rv2ide/activities/TomIDEUpdater.kt | 2 +- updater.json | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/activities/TomIDEUpdater.kt b/core/app/src/main/java/com/tom/rv2ide/activities/TomIDEUpdater.kt index 6dd4d518d1..558d27fc83 100644 --- a/core/app/src/main/java/com/tom/rv2ide/activities/TomIDEUpdater.kt +++ b/core/app/src/main/java/com/tom/rv2ide/activities/TomIDEUpdater.kt @@ -43,7 +43,7 @@ class TomIDEUpdater(private val context: Context) { companion object { private const val TAG = "TomIDEUpdater" private const val UPDATE_JSON_URL = - "https://raw.githubusercontent.com/AndroidCSOfficial/android-code-studio/refs/heads/dev/updater.json" + "https://raw.githubusercontent.com/xeriomy/acs-zen/refs/heads/dev/updater.json" private const val DOWNLOAD_NOTIFICATION_ID = 1001 } diff --git a/updater.json b/updater.json index a821f5de19..08efabe1df 100644 --- a/updater.json +++ b/updater.json @@ -1,18 +1,18 @@ { - "baseVersionCode": 1025, - "baseVersionName": "1.0.0+gh.r4", - "baseUrl": "https://github.com/Mohammed-Baqer-null/AndroidIDE-Rv2/releases/download/v{versionName}", + "baseVersionCode": 1024, + "baseVersionName": "1.0.0+gh.r04", + "baseUrl": "https://github.com/xeriomy/acs-zen/releases/download/v{versionName}", "variants": { "armeabi-v7a": { - "versionCode": 1025, - "versionName": "1.0.0+gh.r4", + "versionCode": 1024, + "versionName": "1.0.0+gh.r04", "apkUrl": "{baseUrl}/android-code-studio-armeabi-v7a-{versionName}.apk" }, "arm64-v8a": { - "versionCode": 1025, - "versionName": "1.0.0+gh.r4", + "versionCode": 1024, + "versionName": "1.0.0+gh.r04", "apkUrl": "{baseUrl}/android-code-studio-aarch64-{versionName}.apk" } }, - "changelog": "https://raw.githubusercontent.com/AndroidCSOfficial/android-code-studio/refs/heads/dev/whatsnew/v1.0.0+gh.r3.md" + "changelog": "https://raw.githubusercontent.com/xeriomy/acs-zen/refs/heads/dev/whatsnew/v1.0.0+gh.r3.md" } From 3fbd6feb9742fdf6820d45e9df7d8b5ad4b6399f Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 23 Sep 2026 12:29:03 +0300 Subject: [PATCH 06/23] feat(core/app): redesign new project template picker --- .../tom/rv2ide/templates/AtcWizardDialog.kt | 49 ++++++++++++------- .../src/main/res/layout/dialog_atc_wizard.xml | 13 +++-- .../resources/src/main/res/values/strings.xml | 1 + 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt b/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt index 2a95e15f38..b8465a67ee 100644 --- a/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt +++ b/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt @@ -150,7 +150,10 @@ class AtcWizardDialog : BottomSheetDialogFragment() { private fun setupTemplatesGrid(ctx: Context) { val templates = TemplateRegistry.getAllTemplates() - binding.templatesGrid.layoutManager = GridLayoutManager(ctx, 2) + // Adaptive columns: 2 on phones, more on wider screens (landscape/tablet). + val screenWidthDp = ctx.resources.displayMetrics.widthPixels / ctx.resources.displayMetrics.density + val spanCount = (screenWidthDp / 180).toInt().coerceAtLeast(2) + binding.templatesGrid.layoutManager = GridLayoutManager(ctx, spanCount) binding.templatesGrid.adapter = TemplateAdapter(ctx, templates) { template -> selectedTemplate = template @@ -461,10 +464,6 @@ class TemplateAdapter( ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TemplateVH { - val displayMetrics = ctx.resources.displayMetrics - val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density - android.util.Log.d("TemplateAdapter", "Screen Width DP: $screenWidthDp") - val card = MaterialCardView(ctx).apply { layoutParams = @@ -472,35 +471,46 @@ class TemplateAdapter( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, ) - .apply { setMargins(8.dp, 8.dp, 8.dp, 8.dp) } + .apply { setMargins(6.dp, 6.dp, 6.dp, 6.dp) } radius = 20.dp.toFloat() isClickable = true isFocusable = true - strokeWidth = 0 - elevation = 1.dp.toFloat() + strokeWidth = 1.dp + strokeColor = + com.google.android.material.color.MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorOutlineVariant, + ) + elevation = 0f + + val surfaceContainerHigh = + com.google.android.material.color.MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurfaceContainerHigh, + ) + setCardBackgroundColor(surfaceContainerHigh) + + val outValue = android.util.TypedValue() + context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) + foreground = context.getDrawable(outValue.resourceId) } val layout = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL } val title = TextView(ctx).apply { textSize = 14f + typeface = android.graphics.Typeface.DEFAULT_BOLD setPadding(12.dp, 12.dp, 12.dp, 8.dp) gravity = android.view.Gravity.CENTER + maxLines = 2 + ellipsize = android.text.TextUtils.TruncateAt.END } - + val image = ImageView(ctx).apply { scaleType = ImageView.ScaleType.CENTER_CROP - - if (screenWidthDp >= 600) { - android.util.Log.d("TemplateAdapter", "Using small size for large screen") - layoutParams = LinearLayout.LayoutParams(100.dp, 100.dp).apply { - gravity = android.view.Gravity.CENTER - } - } else { - android.util.Log.d("TemplateAdapter", "Using normal size") - layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 160.dp) - } + // Fixed preview height keeps every card consistent in any column count. + layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 120.dp) } layout.addView(title) @@ -512,6 +522,7 @@ class TemplateAdapter( override fun onBindViewHolder(holder: TemplateVH, position: Int) { val template = templates[position] holder.title.text = template.displayName + holder.card.contentDescription = template.displayName val resId = ctx.resources.getIdentifier( diff --git a/core/app/src/main/res/layout/dialog_atc_wizard.xml b/core/app/src/main/res/layout/dialog_atc_wizard.xml index 5583d72e19..d6529ee121 100644 --- a/core/app/src/main/res/layout/dialog_atc_wizard.xml +++ b/core/app/src/main/res/layout/dialog_atc_wizard.xml @@ -24,10 +24,17 @@ + + + android:text="@string/new_project_template_subtitle" + android:textAppearance="?attr/textAppearanceBodyMedium" + android:textColor="?attr/colorOnSurfaceVariant" /> Choose Template + Choose a template to get started. Project Configuration Project name From 2320409db7805e5d19bad3a8e462f499c8a67c57 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 23 Sep 2026 13:56:15 +0300 Subject: [PATCH 07/23] feat(core/app): redesign new project options --- .../tom/rv2ide/templates/AtcWizardDialog.kt | 1 + .../src/main/res/layout/dialog_atc_wizard.xml | 161 ++++++++++++------ .../resources/src/main/res/values/strings.xml | 6 + 3 files changed, 118 insertions(+), 50 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt b/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt index b8465a67ee..ece109ec86 100644 --- a/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt +++ b/core/app/src/main/java/com/tom/rv2ide/templates/AtcWizardDialog.kt @@ -244,6 +244,7 @@ class AtcWizardDialog : BottomSheetDialogFragment() { binding.packageNameInput.setText("com.example.$packageSuffix") val isNative = Options.OPT_IS_NATIVE_CPP + binding.nativeSection.visibility = if (isNative) View.VISIBLE else View.GONE binding.useCMakeSwitch.visibility = if (isNative) View.VISIBLE else View.GONE binding.nativeLanguageInputLayout.visibility = if (isNative) View.VISIBLE else View.GONE binding.ndkVersionButton.visibility = if (isNative) View.VISIBLE else View.GONE diff --git a/core/app/src/main/res/layout/dialog_atc_wizard.xml b/core/app/src/main/res/layout/dialog_atc_wizard.xml index d6529ee121..a159a66635 100644 --- a/core/app/src/main/res/layout/dialog_atc_wizard.xml +++ b/core/app/src/main/res/layout/dialog_atc_wizard.xml @@ -55,17 +55,33 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/project_configuration" - android:textAppearance="?attr/textAppearanceHeadlineSmall" - android:layout_marginBottom="16dp" - android:gravity="center" /> + android:textAppearance="?attr/textAppearanceHeadlineSmall" /> + + + + + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:hint="@string/project_name" + style="@style/Widget.Material3.TextInputLayout.FilledBox"> + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:hint="@string/package_name" + style="@style/Widget.Material3.TextInputLayout.FilledBox"> + + @@ -105,11 +131,19 @@ android:layout_height="wrap_content" /> + + @@ -124,8 +158,8 @@ @@ -137,61 +171,86 @@ android:focusable="false" /> - + + android:orientation="vertical" + android:visibility="gone"> - - + android:layout_marginTop="12dp" + android:text="@string/options_section_native" + android:textAppearance="?attr/textAppearanceTitleSmall" + android:textColor="?attr/colorOnSurfaceVariant" /> + + + + + + + + + + - + android:layout_marginTop="12dp" + android:text="@string/options_section_build" + android:textAppearance="?attr/textAppearanceTitleSmall" + android:textColor="?attr/colorOnSurfaceVariant" /> - - @@ -199,6 +258,7 @@ android:id="@+id/backButton" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minHeight="48dp" android:text="@string/back" android:visibility="gone" style="@style/Widget.Material3.Button.TextButton" /> @@ -211,6 +271,7 @@ android:id="@+id/createButton" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minHeight="48dp" android:text="@string/create" android:visibility="gone" /> diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index 7da3807d2c..0685d25cd3 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -243,6 +243,12 @@ Choose Template Choose a template to get started. Project Configuration + Set the name, package and build options for your new project. + Project + Location + Android + Native + Build Project name Project location From 42e1d66b5734c753f33089dc055ee7b8fb875ddc Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 23 Sep 2026 15:53:22 +0300 Subject: [PATCH 08/23] feat(core/app): redesign project management --- .../com/tom/rv2ide/fragments/MainFragment.kt | 92 +++++++++++++++---- .../res/layout/bottomsheet_project_list.xml | 70 ++++++++++---- core/app/src/main/res/layout/item_project.xml | 32 +++++-- .../resources/src/main/res/values/strings.xml | 6 ++ 4 files changed, 150 insertions(+), 50 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt index 67390662b6..22e13a2bdb 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.fragment.app.viewModels @@ -29,7 +30,9 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip +import com.google.android.material.color.MaterialColors import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout @@ -238,6 +241,9 @@ class MainFragment : BaseFragment() { val btnBrowse = sheetView.findViewById(R.id.btnBrowse) val searchEditText = sheetView.findViewById(R.id.searchEditText) + sheetView + .findViewById(R.id.btnCloseProjects) + ?.setOnClickListener { bottomSheet.dismiss() } recyclerView.layoutManager = LinearLayoutManager(requireContext()) @@ -307,25 +313,70 @@ class MainFragment : BaseFragment() { } private fun showProjectOptionsDialog(project: File, onActionComplete: () -> Unit) { - val options = arrayOf("Backup project", "Delete project", "Rename") - - val builder = DialogUtils.newMaterialDialogBuilder(requireContext()) - builder.setTitle(project.name) - builder.setItems(options) { dialog, which -> - when (which) { - 0 -> { - backupProject(project, onActionComplete) + val context = requireContext() + val sheet = BottomSheetDialog(context) + + val padding = (16 * context.resources.displayMetrics.density).toInt() + val container = + LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding(padding, padding, padding, padding + padding / 2) } - 1 -> { - showDeleteProjectConfirmation(project, onActionComplete) - } - 2 -> { - showRenameDialog(project, onActionComplete) + + val title = + TextView(context).apply { + text = project.name + setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_TitleMedium) + setPadding(padding / 2, 0, padding / 2, padding / 2) } - } - dialog.dismiss() + container.addView(title) + + fun addActionRow(text: String, isDestructive: Boolean, onClick: () -> Unit) { + val button = + MaterialButton( + context, + null, + com.google.android.material.R.attr.materialButtonTextButtonStyle, + ) + .apply { + this.text = text + textAlignment = View.TEXT_ALIGNMENT_TEXT_START + gravity = android.view.Gravity.START or android.view.Gravity.CENTER_VERTICAL + minHeight = (56 * context.resources.displayMetrics.density).toInt() + layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + if (isDestructive) { + setTextColor( + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorError, + ) + ) + } + setOnClickListener { + sheet.dismiss() + onClick() + } + } + container.addView(button) } - builder.show() + + // Same targets as before; only the presentation changed. + addActionRow(getString(string.project_action_backup), false) { + backupProject(project, onActionComplete) + } + addActionRow(getString(string.project_action_rename), false) { + showRenameDialog(project, onActionComplete) + } + addActionRow(getString(string.project_action_delete), true) { + showDeleteProjectConfirmation(project, onActionComplete) + } + + sheet.setContentView(container) + sheet.show() } private fun showRenameDialog(project: File, onComplete: () -> Unit) { @@ -801,7 +852,7 @@ class MainFragment : BaseFragment() { inner class RecentViewHolder(view: View) : RecyclerView.ViewHolder(view) { val projectName: TextView = view.findViewById(R.id.projectName) val projectPath: TextView = view.findViewById(R.id.projectPath) - val recentBadge: TextView = view.findViewById(R.id.recentBadge) + val recentBadgeContainer: View = view.findViewById(R.id.recentBadgeContainer) val root: View = view } @@ -817,7 +868,8 @@ class MainFragment : BaseFragment() { val recentRank = WizardPreferences.getRecentProjectRank(holder.root.context, project.absolutePath) - holder.recentBadge.visibility = if (recentRank in 0..2) View.VISIBLE else View.GONE + holder.recentBadgeContainer.visibility = + if (recentRank in 0..2) View.VISIBLE else View.GONE holder.root.setOnClickListener { onProjectClick(project) } } @@ -836,7 +888,7 @@ class MainFragment : BaseFragment() { inner class ProjectViewHolder(view: View) : RecyclerView.ViewHolder(view) { val projectName: TextView = view.findViewById(R.id.projectName) val projectPath: TextView = view.findViewById(R.id.projectPath) - val recentBadge: TextView = view.findViewById(R.id.recentBadge) + val recentBadgeContainer: View = view.findViewById(R.id.recentBadgeContainer) val root: View = view } @@ -853,7 +905,7 @@ class MainFragment : BaseFragment() { val recentRank = WizardPreferences.getRecentProjectRank(holder.root.context, project.absolutePath) val isRecent = recentRank in 0..2 // Top 3 most recent projects - holder.recentBadge.visibility = if (isRecent) View.VISIBLE else View.GONE + holder.recentBadgeContainer.visibility = if (isRecent) View.VISIBLE else View.GONE holder.root.setOnClickListener { onProjectClick(project) } diff --git a/core/app/src/main/res/layout/bottomsheet_project_list.xml b/core/app/src/main/res/layout/bottomsheet_project_list.xml index 01c5060fe1..b612c7bea1 100644 --- a/core/app/src/main/res/layout/bottomsheet_project_list.xml +++ b/core/app/src/main/res/layout/bottomsheet_project_list.xml @@ -1,38 +1,52 @@ + android:paddingHorizontal="20dp" + android:paddingTop="12dp" + android:paddingBottom="24dp"> - + + android:paddingBottom="4dp"> - + android:orientation="vertical"> + + + + + + + android:id="@+id/btnCloseProjects" + style="@style/Widget.Material3.Button.IconButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:contentDescription="@string/projects_close" + app:icon="@drawable/ic_close" + app:iconTint="?attr/colorOnSurfaceVariant" /> @@ -40,7 +54,8 @@ android:id="@+id/searchLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="4dp" android:hint="@string/search_projects_hint" app:startIconDrawable="@drawable/ic_search" app:endIconMode="clear_text" @@ -73,6 +88,21 @@ android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textColor="?attr/colorOnSurfaceVariant" android:padding="32dp" - android:visibility="gone" /> + android:visibility="gone" + tools:visibility="visible" /> + + + - \ No newline at end of file + diff --git a/core/app/src/main/res/layout/item_project.xml b/core/app/src/main/res/layout/item_project.xml index d2183d51ae..b2c2744830 100644 --- a/core/app/src/main/res/layout/item_project.xml +++ b/core/app/src/main/res/layout/item_project.xml @@ -8,6 +8,7 @@ android:clickable="true" android:focusable="true" android:foreground="?attr/selectableItemBackground" + android:minHeight="72dp" app:cardCornerRadius="16dp" app:strokeWidth="0dp" app:cardBackgroundColor="?attr/colorSurfaceContainerHigh" @@ -44,6 +45,7 @@ android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginStart="12dp" + android:layout_marginEnd="4dp" android:orientation="vertical"> - + tools:visibility="visible" + app:cardElevation="0dp" + app:strokeWidth="0dp" + app:cardCornerRadius="8dp" + app:cardBackgroundColor="?attr/colorSecondaryContainer"> + + + + diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index 0685d25cd3..5cfb325cfc 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -189,6 +189,12 @@ Close project Open Project + Projects + Open, organize and back up your projects. + Close + Backup project + Delete project + Rename Search projects… No projects found.\nCreate a new project or browse to open an existing one. No projects match your search. From af8cf370534b0289c933cc6adccb2ca42c68229e Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 23 Sep 2026 16:06:00 +0300 Subject: [PATCH 09/23] fix(core/app): use appcompat attrs for project actions sheet --- .../src/main/java/com/tom/rv2ide/fragments/MainFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt index 22e13a2bdb..b160af31aa 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt @@ -336,7 +336,7 @@ class MainFragment : BaseFragment() { MaterialButton( context, null, - com.google.android.material.R.attr.materialButtonTextButtonStyle, + androidx.appcompat.R.attr.borderlessButtonStyle, ) .apply { this.text = text @@ -352,7 +352,7 @@ class MainFragment : BaseFragment() { setTextColor( MaterialColors.getColor( this, - com.google.android.material.R.attr.colorError, + androidx.appcompat.R.attr.colorError, ) ) } From 6b932f32c7e173bb59fbd788dc6b24c8061636aa Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 23 Sep 2026 20:17:58 +0300 Subject: [PATCH 10/23] feat(core/app): redesign settings root --- .../rv2ide/activities/PreferencesActivity.kt | 3 +- .../fragments/IDEPreferencesFragment.kt | 82 ++++++++++++++++++- .../src/main/res/layout/preference_header.xml | 26 ++++++ .../resources/src/main/res/values/strings.xml | 2 + 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 core/app/src/main/res/layout/preference_header.xml diff --git a/core/app/src/main/java/com/tom/rv2ide/activities/PreferencesActivity.kt b/core/app/src/main/java/com/tom/rv2ide/activities/PreferencesActivity.kt index 82ff92ffcc..ac9d82b0c0 100644 --- a/core/app/src/main/java/com/tom/rv2ide/activities/PreferencesActivity.kt +++ b/core/app/src/main/java/com/tom/rv2ide/activities/PreferencesActivity.kt @@ -47,7 +47,7 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() { super.onCreate(savedInstanceState) setSupportActionBar(binding.toolbar) - supportActionBar!!.setTitle(R.string.ide_preferences) + supportActionBar!!.setTitle(R.string.settings_title) supportActionBar!!.setDisplayHomeAsUpEnabled(true) binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } @@ -62,6 +62,7 @@ class PreferencesActivity : EdgeToEdgeIDEActivity() { val args = Bundle() args.putParcelableArrayList(IDEPreferencesFragment.EXTRA_CHILDREN, ArrayList(prefs.children)) + args.putBoolean(IDEPreferencesFragment.EXTRA_IS_ROOT, true) rootFragment.arguments = args loadFragment(rootFragment) diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/IDEPreferencesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/IDEPreferencesFragment.kt index 7c3ca17e30..d67f1a4335 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/IDEPreferencesFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/IDEPreferencesFragment.kt @@ -17,13 +17,20 @@ package com.tom.rv2ide.fragments +import android.content.Context +import android.graphics.drawable.Drawable import android.os.Bundle +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceGroup import com.google.android.material.transition.MaterialSharedAxis +import com.tom.rv2ide.R import com.tom.rv2ide.preferences.IPreference import com.tom.rv2ide.preferences.IPreferenceGroup import com.tom.rv2ide.preferences.IPreferenceScreen @@ -73,7 +80,13 @@ class IDEPreferencesFragment : BasePreferenceFragment() { this.children = arguments?.getParcelableArrayList(EXTRA_CHILDREN) ?: emptyList() preferenceScreen.removeAll() + if (isRootScreen()) { + addHeader() + } addChildren(this.children, preferenceScreen) + if (isRootScreen()) { + styleRootRows() + } } override fun onResume() { @@ -92,7 +105,13 @@ class IDEPreferencesFragment : BasePreferenceFragment() { } preferenceScreen.removeAll() + if (isRootScreen()) { + addHeader() + } addChildren(this.children, preferenceScreen) + if (isRootScreen()) { + styleRootRows() + } } private fun addChildren(children: List, pref: PreferenceGroup) { @@ -117,5 +136,66 @@ class IDEPreferencesFragment : BasePreferenceFragment() { companion object { const val EXTRA_CHILDREN = "ide.preferences.fragment.children" + const val EXTRA_IS_ROOT = "ide.preferences.fragment.is_root" + + /** + * Root-screen row icons by preference key. Central mapping so individual + * *PrefExts files stay untouched. Keys and navigation are unchanged. + */ + private val ROOT_ROW_ICONS = + mapOf( + "idepref_general" to R.drawable.ic_settings, + "idepref_editor" to R.drawable.ic_code, + "idepref_ai_agent" to R.drawable.ic_ai_agent, + "idepref_build_n_run" to R.drawable.ic_run, + "ide.preferences.terminal" to R.drawable.ic_terminal, + "ide.prefs.developerOptions" to R.drawable.ic_bug, + "idepref_changelog" to R.drawable.ic_history, + "idepref_about" to R.drawable.ic_info, + ) + } + + private fun isRootScreen(): Boolean = arguments?.getBoolean(EXTRA_IS_ROOT) == true + + /** + * Root-only presentation. Nested screens keep the stock AndroidX look: + * they are created by the framework without EXTRA_IS_ROOT. + */ + private fun addHeader() { + val header = + Preference(requireContext()).apply { + key = "acszen_settings_header" + layoutResource = R.layout.preference_header + isSelectable = false + } + preferenceScreen.addPreference(header) + } + + private fun styleRootRows() { + val context = requireContext() + for (i in 0 until preferenceScreen.preferenceCount) { + val preference = preferenceScreen.getPreference(i) + // Uniform alignment, including the rows without an icon. + preference.isIconSpaceReserved = true + ROOT_ROW_ICONS[preference.key]?.let { resId -> + tintedIcon(context, resId)?.let { preference.icon = it } + } + } + } + + private fun tintedIcon(context: Context, resId: Int): Drawable? { + val drawable = ContextCompat.getDrawable(context, resId)?.mutate() ?: return null + DrawableCompat.setTint(drawable, secondaryTextColor(context)) + return drawable + } + + private fun secondaryTextColor(context: Context): Int { + val typedValue = TypedValue() + if (context.theme.resolveAttribute(android.R.attr.textColorSecondary, typedValue, true) && + typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && + typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return typedValue.data + } + return ContextCompat.getColor(context, android.R.color.darker_gray) } -} \ No newline at end of file +} diff --git a/core/app/src/main/res/layout/preference_header.xml b/core/app/src/main/res/layout/preference_header.xml new file mode 100644 index 0000000000..f969149d7d --- /dev/null +++ b/core/app/src/main/res/layout/preference_header.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index 5cfb325cfc..9947db0bc6 100644 --- a/core/resources/src/main/res/values/strings.xml +++ b/core/resources/src/main/res/values/strings.xml @@ -120,6 +120,8 @@ translatable="false">GitHub Need Help? IDE preferences + Settings + Customize the IDE appearance, tools and workflow. Discussions on Telegram Official Telegram Channel No data From 600333dd35489ee930b0db153a2d255fbdfa0625 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 23 Sep 2026 21:04:54 +0300 Subject: [PATCH 11/23] fix(core/app): remove duplicate settings header --- core/app/src/main/res/layout/preference_header.xml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/core/app/src/main/res/layout/preference_header.xml b/core/app/src/main/res/layout/preference_header.xml index f969149d7d..e8ebc42f32 100644 --- a/core/app/src/main/res/layout/preference_header.xml +++ b/core/app/src/main/res/layout/preference_header.xml @@ -1,24 +1,17 @@ - + - - From 8907cfc6ed8c28f677c19e043a35222df1c42507 Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 24 Sep 2026 00:22:57 +0300 Subject: [PATCH 12/23] ci: clean up GitHub Actions build warnings --- .github/workflows/asm_build.yml | 36 +++++++++------------------------ 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/.github/workflows/asm_build.yml b/.github/workflows/asm_build.yml index e22190f9cc..14680713bd 100644 --- a/.github/workflows/asm_build.yml +++ b/.github/workflows/asm_build.yml @@ -33,15 +33,13 @@ on: jobs: build_release_apk: name: Build APK - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + concurrency: + group: build-apk-${{ github.ref }} + cancel-in-progress: true steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.12.1 - with: - access_token: ${{ github.token }} - - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: 'recursive' fetch-depth: 0 @@ -54,10 +52,10 @@ jobs: echo "BUILD_TYPE_LOWER=$(echo "$BUILD_TYPE" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '21' - distribution: 'adopt' + distribution: 'temurin' - name: Generating missing dirs run: mkdir -p core/app/build/intermediates/external_file_lib_dex_archives/debug/desugarDebugFileDependencies && mkdir -p core/app/build/intermediates/l8_art_profile/release/l8DexDesugarLibRelease @@ -66,7 +64,7 @@ jobs: run: chmod +x ./gradlew - name: Restore cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -92,29 +90,15 @@ jobs: run: ls -la core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/ || echo "Directory not found" - name: Upload arm64-v8a APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: apk-arm64-v8a-${{ env.BUILD_TYPE_LOWER }} path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*arm64-v8a*.apk if: always() - name: Upload armeabi-v7a APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: apk-armeabi-v7a-${{ env.BUILD_TYPE_LOWER }} path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*armeabi-v7a*.apk if: always() - - - name: Upload x86_64 APK - uses: actions/upload-artifact@v4 - with: - name: apk-x86_64-${{ env.BUILD_TYPE_LOWER }} - path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*x86_64*.apk - if: always() - - - name: Upload universal APK - uses: actions/upload-artifact@v4 - with: - name: apk-universal-${{ env.BUILD_TYPE_LOWER }} - path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*universal*.apk - if: always() From 8fccfe3a4d19305f9f937e92f37e13d5aaacc7ae Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 24 Sep 2026 00:35:41 +0300 Subject: [PATCH 13/23] ci: use upload-artifact v6 for Node 24 runtime --- .github/workflows/asm_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/asm_build.yml b/.github/workflows/asm_build.yml index 14680713bd..29139d9f88 100644 --- a/.github/workflows/asm_build.yml +++ b/.github/workflows/asm_build.yml @@ -90,14 +90,14 @@ jobs: run: ls -la core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/ || echo "Directory not found" - name: Upload arm64-v8a APK - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: apk-arm64-v8a-${{ env.BUILD_TYPE_LOWER }} path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*arm64-v8a*.apk if: always() - name: Upload armeabi-v7a APK - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: apk-armeabi-v7a-${{ env.BUILD_TYPE_LOWER }} path: core/app/build/outputs/apk/${{ env.BUILD_TYPE_LOWER }}/*armeabi-v7a*.apk From c9def827bd37ca47f23f5ef70f813b6426932ac3 Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 27 Sep 2026 00:11:54 +0300 Subject: [PATCH 14/23] fix(git): expose all remote branches after clone and allow checking them out A clone only creates a local branch for the remote's default branch while every remote branch is fetched into refs/remotes/*, but getAllBranches() listed local branches only. That made a freshly cloned repository look like it had exactly one branch, and checkout of any other branch failed with "Ref cannot be resolved" because JGit has no DWIM rule that maps a remote-only branch name to its remote-tracking ref. - list local and remote-tracking branches together (ListMode.ALL), deduplicated, sorted, skipping the symbolic remote HEAD ref - check out remote-only branches by creating a local tracking branch from the matching remote-tracking ref with upstream configuration set, matching native `git checkout ` semantics --- .../java/com/tom/rv2ide/git/GitManager.kt | 84 +++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/git/GitManager.kt b/core/app/src/main/java/com/tom/rv2ide/git/GitManager.kt index 3768bb3fe1..6c000fe1b0 100644 --- a/core/app/src/main/java/com/tom/rv2ide/git/GitManager.kt +++ b/core/app/src/main/java/com/tom/rv2ide/git/GitManager.kt @@ -297,16 +297,39 @@ class GitManager(private val projectPath: String) { } } + /** + * Lists every branch the repository knows about: local branches + * (refs/heads/...) and remote-tracking branches (refs/remotes/...). + * + * A clone only creates a local branch for the remote's default branch, so + * listing local branches alone makes a freshly cloned repository look like + * it has exactly one branch. Remote branches are deduplicated against + * local ones and the symbolic remote HEAD ref is skipped. + */ fun getAllBranches(): List { - val branches = mutableListOf() + val branches = linkedSetOf() try { - git?.branchList()?.call()?.forEach { ref -> - branches.add(ref.name.removePrefix("refs/heads/")) - } + git?.branchList() + ?.setListMode(org.eclipse.jgit.api.ListBranchCommand.ListMode.ALL) + ?.call() + ?.forEach { ref -> + val name = ref.name + when { + name.startsWith(Constants.R_HEADS) -> + branches.add(name.removePrefix(Constants.R_HEADS)) + name.startsWith(Constants.R_REMOTES) -> { + val remoteBranch = name.removePrefix(Constants.R_REMOTES) + val branchName = remoteBranch.substringAfter('/', "") + if (branchName.isNotEmpty() && branchName != "HEAD") { + branches.add(branchName) + } + } + } + } } catch (e: Exception) { e.printStackTrace() } - return branches + return branches.sorted() } fun createBranch(branchName: String): Boolean { @@ -321,13 +344,62 @@ class GitManager(private val projectPath: String) { fun checkoutBranch(branchName: String): Boolean { return try { - git?.checkout()?.setName(branchName)?.call() + val repo = repository + val localRef = repo?.exactRef(Constants.R_HEADS + branchName) + if (localRef != null) { + git?.checkout()?.setName(branchName)?.call() + } else { + val remoteRef = findRemoteTrackingRef(branchName) + if (remoteRef != null) { + // Remote-only branch: create the local tracking branch from + // its remote-tracking ref first (like `git checkout ` + // does), then switch to it. + git?.checkout() + ?.setName(branchName) + ?.setCreateBranch(true) + ?.setStartPoint(remoteRef) + ?.setUpstreamMode( + org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode.SET_UPSTREAM + ) + ?.call() + } else { + git?.checkout()?.setName(branchName)?.call() + } + } true } catch (e: Exception) { e.printStackTrace() false } } + + /** + * Finds the remote-tracking ref (e.g. refs/remotes/origin/feature) matching + * [branchName]. The remote named 'origin' is preferred, remaining remotes + * are considered in alphabetical order. + */ + private fun findRemoteTrackingRef(branchName: String): String? { + val repo = repository ?: return null + return try { + repo.refDatabase.getRefsByPrefix(Constants.R_REMOTES) + .map { it.name } + .filter { name -> + val remoteBranch = name.removePrefix(Constants.R_REMOTES) + val branch = remoteBranch.substringAfter('/', "") + branch.isNotEmpty() && branch != "HEAD" && branch == branchName + } + .sortedWith( + compareBy( + { it.removePrefix(Constants.R_REMOTES).substringBefore('/') != "origin" }, + { it } + ) + ) + .firstOrNull() + } catch (e: Exception) { + e.printStackTrace() + null + } + } fun deleteBranch(branchName: String): Boolean { return try { From a74f749c3037097ba7bd06c1dd0249a739812c3f Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 27 Sep 2026 02:17:22 +0300 Subject: [PATCH 15/23] fix(build): keep desugar_jdk_libs runtime packaged in the APK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tasks.configureEach disabled every task whose name contains "desugar", which also matched `l8DexDesugarLib` — the AGP task that produces the desugar_jdk_libs runtime dex which PackageAndroidArtifact packs into the APK. Debug APKs therefore shipped with no `j$` classes at all (verified: zero `Lj$/...` class definitions in every classes*.dex of run 36273739542), so any desugared JDK call died at runtime. JGit 6.8's RefDirectory.getRefsByPrefix(varargs) — the overload used only when several prefixes are passed, i.e. ListMode.ALL branch listing — calls java.util.stream.Collectors.toUnmodifiableList() (Java 10), which D8 rewrites to j$.util.stream.DesugarCollectors. Opening the Git branches screen therefore crashed with NoClassDefFoundError. Narrow the predicate to `desugarFileDependencies` only, the task CI already works around by pre-creating its output directory. --- core/app/build.gradle.kts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/app/build.gradle.kts b/core/app/build.gradle.kts index 8a6c764916..f86fb6e32e 100755 --- a/core/app/build.gradle.kts +++ b/core/app/build.gradle.kts @@ -42,8 +42,20 @@ buildscript { } } +// AGP's file-dependency desugaring task (`desugarFileDependencies`) is disabled +// in this project; CI pre-creates its output directory instead +// (see the "Generating missing dirs" step in .github/workflows/asm_build.yml). +// +// The match below is deliberately narrow. AGP also has tasks named +// `l8DexDesugarLib`, which contain "Desugar" as well, and those must keep +// running: they produce the `desugar_jdk_libs` runtime dex that PackageAndroidArtifact +// packs into the APK whenever core library desugaring is enabled. With them disabled the +// APK contains no `j$` classes at all, so every desugared JDK call throws at runtime, +// e.g. JGit's `java.util.stream.Collectors.toUnmodifiableList()` (used by +// `RefDirectory.getRefsByPrefix(varargs)`, i.e. `ListMode.ALL` branch listing) fails with +// `NoClassDefFoundError: j$.util.stream.DesugarCollectors`. tasks.configureEach { - if (name.contains("desugar", ignoreCase = true)) { + if (name.startsWith("desugar") && name.endsWith("FileDependencies")) { enabled = false } } From c88326de01abbba935a2b65027d32781ba65393d Mon Sep 17 00:00:00 2001 From: opencode Date: Sun, 27 Sep 2026 02:29:33 +0300 Subject: [PATCH 16/23] fix(core/app): sign Debug builds with a stable committed debug keystore Each GitHub Actions run generated a brand-new ~/.android/debug.keystore: AGP's SigningConfigFactory defaults the "debug" signing config to that path, the runner has no persistent copy (the workflow caches only ~/.gradle), and AGP creates the file when it is missing. Every Debug APK therefore shipped with a different signer certificate: run 36273739542 -> sha256 1e3bb6e9f39d0033b8b169b934b65eef0bee901b... run 35924946644 -> sha256 5000db600eb65c0567d012cf85a57f1b1296d71a... package (com.tom.rv2ide), versionCode (1024), versionName and the whole manifest were identical between those two builds, so the failure was not version ordering or application id: Android rejected the newer build over the installed one because the signatures did not match, and every update needed an uninstall first. Commit one stock Android debug keystore (androiddebugkey/android) and point signingConfigs.debug at it, so CI runs and developer machines all produce identically signed Debug APKs. Release signing and its credentials are untouched, and Debug builds still need no secrets. --- core/app/build.gradle.kts | 20 +++++++++++++++++++- signing/debug.keystore | Bin 0 -> 2666 bytes 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 signing/debug.keystore diff --git a/core/app/build.gradle.kts b/core/app/build.gradle.kts index 8a6c764916..a849dc2837 100755 --- a/core/app/build.gradle.kts +++ b/core/app/build.gradle.kts @@ -74,6 +74,23 @@ android { signingConfigs { + // One stable, repo-committed Android debug keystore, used by CI and by local builds + // alike. AGP otherwise defaults to ~/.android/debug.keystore (see AGP's + // SigningConfigFactory, which applies DefaultSigningConfig.DebugSigningConfig for the + // "debug" name). That file does not exist on a GitHub Actions runner, so AGP + // generated a brand-new random key on every run: each Debug APK then carried a + // different signer certificate (verified on runs 36273739542 vs 35924946644) and + // Android refused to install the new build over the previous one, forcing an + // uninstall for every update. + // The credentials below are the stock Android debug ones (androiddebugkey/android), + // so Debug builds still need no developer secrets. + getByName("debug") { + storeFile = file("${rootProject.projectDir}/signing/debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + create("custom") { val keyStorePath = "${rootProject.projectDir}/signing/signing-key.jks" val keyStoreFile = file(keyStorePath) @@ -97,7 +114,8 @@ android { buildTypes { debug { - // Standard Android debug keystore (auto-generated by AGP). + // Standard Android debug keystore (signing/debug.keystore, committed with the repo so + // CI and local builds keep one stable signing identity across installs). // Debug builds must not require the release keystore credentials, // so CI forks without signing secrets can still assembleDebug. signingConfig = signingConfigs.getByName("debug") diff --git a/signing/debug.keystore b/signing/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..d6aa4cac9fa7e029f28853d768b332e6475491ea GIT binary patch literal 2666 zcma)8c{tRK7XHl`+c0(_Yj)F2Gh-RUm>~(-hVYe;os5JJWv8)!Lfw=wg)C8&B}>Uf zB}0uh`x-S_M|P52_qq4!d+uNN{&Al7ocBHN`SX1angaC(gV@j%r~n5yJjpa^8wTP8 z6;Plc2!(qeP2t`}Q`lSnOJc8vP}nOE>GH!W;o$j?ix&z47f{#@4oN+<_iqO~4DEo% z{XO!ck3&SJ*GEwC_q?-itIDpeO$+qZFtg;rU=;x{NDj@*!S(NpU~YB*EzQC0nq&&{ zVuOI-5CNryf?s^%k`q%lMtiyj-x?qkNY7L-PncX-a|L4Sh|h}VT*;+q>-fU1rx(JW z@~edhYkl4vpK4|rNEYc%k5uKHtx!++W8R!pN;Ru}J-KuJ)97{XtMAJny)7*nD&WZV z8FJ!vSy1#~r=|Eis~cr0>|MlMgwTSRkJ4iy>8EkALmi?gm%fpubytM*SFUGccu3*G zZ6}K1^x!}%8R5Y{NHdS$Fscm8kNuEztCJda_^_}VVb5pv@p7Ni5H`zDH86~I=J~bo z9vpNio$6b>2hO11#QFU60pe{ylq!RocnT$u!JB>AT%49HLCK`Pzw&{*oZu*%sEnTaqHug;n z^`d_OF$whN;OV`coZ*RGMySI+NhP9!e@9H8P7`aW{y7*TrN>_-9KD^D>wiifhlY7P z_*7#9I7tjQ#CF8hP>=0P9KZ}LewvHhTCCS!%Az_zxL=gDpQS5$%=fjwdD<+RJiCzo z9!`FD&x#eQEYz9U*OZ}CUWe|(JX9w^uAG3`N2^D@Tz&OxHa2&*M``-r_=acEjr$?G zbuy~luC?^pW;Z;!caKYEO6WE=&)BP=S`U^Zq4Ca7t$r!`@!1O+NgDiZ6B(9YkEb=u zwrfQ}t?G6Oqgpm!PfR%gTx@m;;CwQi^rWX`aulq3ADjOwEKbfo0z5JES5|UyF7Ydy zx-gl65ki}NT=c*mdpSio+ZkwFIh+$6=_(s^!#3ujlx6PmQ~R>NPMJGK+tG?cLDTJg zMNjX&8W7HC3GAr$8oGwP*tMG5LAmd$u#N5VereL-iHF*8W?*gmS|(d5Ux|0K%aaWg zix7BTEK(~*x`yldWWU)9=~%GS>GPNM^U@H8>F|Bp(QE|rmWW* z;R<$%Dgpgygew1|R*zwS&(+CUcZSP%)@7YWzubriv#~>)AK%N%S8J>ySld}0JQQ%a zaZ!%S$bysR0X}RzB{{rZ{MhD+QXPSP*tZ{Kvxv~0_%`->h-Clhm;6M~y!q#JTIvLj zv48%kRKCoK)zb|NuUy*N0;0c52}`^4-E zXQ?6W%*q@??zuORQ)GuxHvGRT+=0#rrJRCx310s?TQpy$trS?)@Ot}^0|^@( zd?VvN>#mwwU#Xb0Xyos>7XeP%JKkeXgi6I={;WLBnG5}Y$G-3K81hPyCNA8f>IZi`}@=TzimA>SRa^rAa1Rbc#D|t zk9&7{Q?a15lwGZsv^ zPyX@2u`~_XCvP|00`>{f2(OvOKbf8{+MH!J;f2E{uW!2J8k{Ruly4(GbTMT|mwH{M z&A(VZkN3VKbnprP2?od+tk}EWC9)Z`Me2Fgx(JT?;&t~j&$xBI4`HO>-AM;Wl9SWU zEe#GZ)*ips=>7bh-f~^y_DL1Bz?dqwd`ZgcOhq!5l_X_ z<^iv6V2rf(STcVUZpKilcI3PIQ!SMYjg5VdFC>b5XIL-iIM2O}>g%EEr0$_iNs%PI zq?;B+fQVPdtf;JaLeu*I34R^4g7X#Gpo2rT{R@UyKb3pfr&aA`k7u~ zp%S?1d#;c_m+=O5iTzT2l_Shpki~e9D@98Ftm>IO-*`|rv^|1qHb+V8x!x3mD(c=% zO?Ju;4ciNVmN}z!Q0{z&Zmd`@67noj4Mh_Gbecx>yG<=@aPv=I3c&PLQ9jFvGw0K(br}Ri8t+St;L{iml{YBzCWX!PkTsczzNW+xpxMu zI*f{XtRJNP1fC5%vvG2Hgk(+0p8pD|nXmKT-r}G`AMNp;*vN#9cNzB$teZ13wb_FZ zTY}P3OU#5OBR;)3v4`0MTvg)mI}ozI Date: Sun, 27 Sep 2026 15:46:52 +0300 Subject: [PATCH 17/23] ui(git): unify the Git client screens around one list-screen scaffold Presentation-only rework of the Git UI. No GitManager, GitViewModel, navigation or backend behaviour is changed. - repository status bar above the tabs (project path + current branch chip) - shared header card on Changes/History/Branches/Remotes: title, subtitle with screen-specific context, single refresh action with ic_sync - loading spinner until the first data emission, and a consistent empty state (title + subtitle) on every list screen - Branches had none - compact horizontal action rows for branch, changed-file and remote items, consistent 18sp titles / 16sp item titles / 14sp secondary type scale - remotes subtitle names the remote that push/pull/fetch act on - destructive confirmations (delete branch, discard changes, remove remote) and their row buttons now use the theme error color - operation results on the Remotes screen reported through snackbars like every other screen instead of modal dialogs; precondition dialogs kept - bottom padding so list content clears the floating action buttons --- .../tom/rv2ide/adapters/BranchesAdapter.kt | 2 + .../tom/rv2ide/fragments/BranchesFragment.kt | 8 +- .../tom/rv2ide/fragments/ChangesFragment.kt | 20 ++- .../tom/rv2ide/fragments/HistoryFragment.kt | 11 +- .../tom/rv2ide/fragments/RemotesFragment.kt | 35 ++-- .../fragments/sidebar/GitClientFragment.kt | 22 +++ .../rv2ide/utils/DestructiveDialogStyle.kt | 50 ++++++ .../src/main/res/layout/fragment_branches.xml | 87 ++++++++-- .../src/main/res/layout/fragment_changes.xml | 112 +++++++++---- .../main/res/layout/fragment_git_client.xml | 74 +++++++-- .../src/main/res/layout/fragment_history.xml | 84 +++++++--- .../src/main/res/layout/fragment_remotes.xml | 151 +++++++++++------- core/app/src/main/res/layout/item_branch.xml | 55 +++++-- core/app/src/main/res/layout/item_commit.xml | 36 +++-- .../src/main/res/layout/item_file_change.xml | 18 +-- core/app/src/main/res/layout/item_remote.xml | 9 +- 16 files changed, 571 insertions(+), 203 deletions(-) create mode 100644 core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt diff --git a/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt b/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt index 0bc0ed5b23..1636d1fb43 100644 --- a/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt +++ b/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt @@ -17,6 +17,7 @@ package com.tom.rv2ide.adapters import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil @@ -57,6 +58,7 @@ class BranchesAdapter( val isCurrentBranch = branch == getCurrentBranch() binding.textBranchName.text = branch + binding.textCurrentBadge.visibility = if (isCurrentBranch) View.VISIBLE else View.GONE if (isCurrentBranch) { val typedValue = TypedValue() diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt index 105f84ce8c..bf120d8697 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt @@ -89,11 +89,16 @@ class BranchesFragment : Fragment() { private fun setupObservers() { viewModel.branches.observe(viewLifecycleOwner) { branches -> adapter.submitList(branches.toList()) + + binding.progressState.visibility = View.GONE + val isEmpty = branches.isEmpty() + binding.recyclerViewBranches.visibility = if (isEmpty) View.GONE else View.VISIBLE + binding.emptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE } viewModel.currentBranch.observe(viewLifecycleOwner) { branch -> currentBranch = branch - binding.textCurrentBranch.text = "Current: $branch" + binding.textCurrentBranch.text = "Current branch: $branch" adapter.notifyDataSetChanged() } @@ -148,6 +153,7 @@ class BranchesFragment : Fragment() { } .setNegativeButton("Cancel", null) .show() + .styleAsDestructive() } override fun onDestroyView() { diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt index c683ac7bce..d1dc4f041b 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt @@ -49,11 +49,22 @@ class ChangesFragment : Fragment() { private fun setupObservers() { viewModel.changedFiles.observe(viewLifecycleOwner) { changes -> adapter.submitList(changes) - binding.emptyStateText.visibility = if (changes.isEmpty()) View.VISIBLE else View.GONE - binding.recyclerViewChanges.visibility = if (changes.isEmpty()) View.GONE else View.VISIBLE - val hasStagedFiles = changes.any { it.isStaged } - binding.fabCommit.visibility = if (hasStagedFiles) View.VISIBLE else View.GONE + binding.progressState.visibility = View.GONE + val isEmpty = changes.isEmpty() + binding.recyclerViewChanges.visibility = if (isEmpty) View.GONE else View.VISIBLE + binding.emptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE + + val stagedCount = changes.count { it.isStaged } + binding.textScreenSubtitle.text = + if (stagedCount == 0) { + "${changes.size} changed" + } else { + "${changes.size} changed · $stagedCount staged" + } + binding.textScreenSubtitle.visibility = if (isEmpty) View.GONE else View.VISIBLE + + binding.fabCommit.visibility = if (stagedCount > 0) View.VISIBLE else View.GONE } viewModel.operationResult.observe(viewLifecycleOwner) { result -> Snackbar.make(binding.root, result.message, Snackbar.LENGTH_SHORT).show() @@ -149,6 +160,7 @@ class ChangesFragment : Fragment() { } .setNegativeButton("Cancel", null) .show() + .styleAsDestructive() } override fun onDestroyView() { diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/HistoryFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/HistoryFragment.kt index 87c8e3c8e0..d1029c1b34 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/HistoryFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/HistoryFragment.kt @@ -68,8 +68,15 @@ class HistoryFragment : Fragment() { private fun setupObservers() { viewModel.commitHistory.observe(viewLifecycleOwner) { commits -> adapter.submitList(commits) - binding.emptyStateText.visibility = if (commits.isEmpty()) View.VISIBLE else View.GONE - binding.recyclerViewHistory.visibility = if (commits.isEmpty()) View.GONE else View.VISIBLE + + binding.progressState.visibility = View.GONE + val isEmpty = commits.isEmpty() + binding.recyclerViewHistory.visibility = if (isEmpty) View.GONE else View.VISIBLE + binding.emptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE + + binding.textScreenSubtitle.text = + if (commits.size == 1) "1 commit" else "${commits.size} commits" + binding.textScreenSubtitle.visibility = if (isEmpty) View.GONE else View.VISIBLE } } diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt index 1899326d75..7b7b2cf37c 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt @@ -83,24 +83,26 @@ class RemotesFragment : Fragment() { private fun setupObservers() { viewModel.remotes.observe(viewLifecycleOwner) { remotes -> adapter.submitList(remotes) - binding.emptyStateText.visibility = if (remotes.isEmpty()) View.VISIBLE else View.GONE - binding.recyclerViewRemotes.visibility = if (remotes.isEmpty()) View.GONE else View.VISIBLE + + binding.progressState.visibility = View.GONE + val isEmpty = remotes.isEmpty() + binding.recyclerViewRemotes.visibility = if (isEmpty) View.GONE else View.VISIBLE + binding.emptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE + + // Push/pull/fetch always act on the first configured remote - make + // that target explicit instead of leaving it to be guessed. + binding.textScreenSubtitle.text = remotes.firstOrNull()?.let { remote -> + "Push, pull and fetch use \"${remote.name}\"" + } ?: "" + binding.textScreenSubtitle.visibility = if (isEmpty) View.GONE else View.VISIBLE } viewModel.operationResult.observe(viewLifecycleOwner) { result -> - if (!result.success) { - showErrorDialog("Operation Failed", result.message) - } else { - Snackbar.make(binding.root, result.message, Snackbar.LENGTH_SHORT).show() - } + showResult(result.success, result.message) } viewModel.pushPullResult.observe(viewLifecycleOwner) { result -> - if (!result.success) { - showErrorDialog("${result.operation.capitalize()} Failed", result.message) - } else { - Snackbar.make(binding.root, result.message, Snackbar.LENGTH_LONG).show() - } + showResult(result.success, result.message) } viewModel.progressMessage.observe(viewLifecycleOwner) { message -> @@ -111,6 +113,14 @@ class RemotesFragment : Fragment() { } } } + + private fun showResult(success: Boolean, message: String) { + Snackbar.make( + binding.root, + message, + if (success) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG + ).show() + } private fun setupButtons() { binding.fabAddRemote.setOnClickListener { @@ -299,6 +309,7 @@ class RemotesFragment : Fragment() { } .setNegativeButton("Cancel", null) .show() + .styleAsDestructive() } override fun onDestroyView() { diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt index a10a2e4341..962e9ff81e 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt @@ -37,6 +37,8 @@ class GitClientFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupRepoHeader() + if (!hasInitialized) { viewModel.checkRepositoryStatus(GCProperties.userProject) setupObservers() @@ -53,6 +55,24 @@ class GitClientFragment : Fragment() { } } + /** + * Repository context shown above the tabs: the project path and the branch + * that is currently checked out. Purely presentational - it only mirrors + * [GitViewModel] state and never triggers Git operations itself. + */ + private fun setupRepoHeader() { + binding.textRepoPath.text = GCProperties.userProject + + viewModel.currentBranch.observe(viewLifecycleOwner) { branch -> + if (branch.isNullOrBlank()) { + binding.textRepoBranch.visibility = View.GONE + } else { + binding.textRepoBranch.text = branch + binding.textRepoBranch.visibility = View.VISIBLE + } + } + } + private fun setupObservers() { viewModel.isRepositoryInitialized.observe(viewLifecycleOwner) { initialized -> if (initialized && !hasInitialized) { @@ -81,6 +101,7 @@ class GitClientFragment : Fragment() { } private fun showInitScreen() { + binding.repoHeader.visibility = View.GONE binding.navigationRail.visibility = View.GONE binding.viewPager.visibility = View.GONE @@ -93,6 +114,7 @@ class GitClientFragment : Fragment() { private fun showMainContent() { binding.containerInit.visibility = View.GONE + binding.repoHeader.visibility = View.VISIBLE binding.navigationRail.visibility = View.VISIBLE binding.viewPager.visibility = View.VISIBLE diff --git a/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt b/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt new file mode 100644 index 0000000000..fef18ebc85 --- /dev/null +++ b/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt @@ -0,0 +1,50 @@ +/* + * This file is part of AndroidCodeStudio. + * + * AndroidCodeStudio is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidCodeStudio is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidCodeStudio. If not, see . +*/ +package com.tom.rv2ide.utils + +import android.content.res.ColorStateList +import android.util.TypedValue +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import com.google.android.material.R + +/** + * Presentation helpers for Git dialogs. + * + * Keeps the visual treatment of destructive confirmations consistent across the + * Git UI without touching any Git behaviour. + * + * @author Mohammed-baqer-null @ https://github.com/Mohammed-baqer-null + */ + +/** + * Colors the confirm button of a destructive dialog (delete branch, remove + * remote, discard changes) with the theme's error color so that it cannot be + * mistaken for a routine action. + */ +fun AlertDialog.styleAsDestructive() { + val typedValue = TypedValue() + if (!context.theme.resolveAttribute(R.attr.colorError, typedValue, true)) return + + val colorStateList = if (typedValue.resourceId != 0) { + ContextCompat.getColorStateList(context, typedValue.resourceId) + } else { + ColorStateList.valueOf(typedValue.data) + } + + colorStateList?.let { getButton(AlertDialog.BUTTON_POSITIVE).backgroundTintList = it } +} diff --git a/core/app/src/main/res/layout/fragment_branches.xml b/core/app/src/main/res/layout/fragment_branches.xml index 07fc51eb8f..d822e56c00 100644 --- a/core/app/src/main/res/layout/fragment_branches.xml +++ b/core/app/src/main/res/layout/fragment_branches.xml @@ -18,27 +18,44 @@ app:cardCornerRadius="12dp"> + android:padding="16dp"> - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + + - + android:layout_marginTop="4dp" + android:text="Current branch: main" + android:textSize="14sp" + android:textColor="?android:textColorSecondary"/> @@ -48,7 +65,39 @@ android:layout_height="0dp" android:layout_weight="1" android:paddingHorizontal="16dp" + android:paddingBottom="80dp" android:clipToPadding="false"/> + + + + + + + - \ No newline at end of file + app:icon="@drawable/ic_add"/> + + + diff --git a/core/app/src/main/res/layout/fragment_changes.xml b/core/app/src/main/res/layout/fragment_changes.xml index b2dc71aef9..abcc571756 100644 --- a/core/app/src/main/res/layout/fragment_changes.xml +++ b/core/app/src/main/res/layout/fragment_changes.xml @@ -21,40 +21,50 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="0dp"> + android:padding="16dp"> - + + + + + + + + - - - - @@ -64,18 +74,54 @@ android:layout_height="0dp" android:layout_weight="1" android:paddingHorizontal="16dp" + android:paddingBottom="80dp" android:clipToPadding="false"/> - + android:padding="32dp" + android:visibility="gone"> + + + + + - - \ No newline at end of file + + + + + diff --git a/core/app/src/main/res/layout/fragment_git_client.xml b/core/app/src/main/res/layout/fragment_git_client.xml index 90ad9a64dc..ccbfb9c7d7 100644 --- a/core/app/src/main/res/layout/fragment_git_client.xml +++ b/core/app/src/main/res/layout/fragment_git_client.xml @@ -8,20 +8,68 @@ + android:orientation="vertical"> - + + + + + + + + - + + + + + + - \ No newline at end of file + diff --git a/core/app/src/main/res/layout/fragment_history.xml b/core/app/src/main/res/layout/fragment_history.xml index b481470322..e7ba929775 100644 --- a/core/app/src/main/res/layout/fragment_history.xml +++ b/core/app/src/main/res/layout/fragment_history.xml @@ -18,26 +18,44 @@ app:cardCornerRadius="12dp"> + android:padding="16dp"> - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + + - + android:layout_marginTop="4dp" + android:textSize="14sp" + android:textColor="?android:textColorSecondary" + android:visibility="gone"/> @@ -49,15 +67,41 @@ android:paddingHorizontal="16dp" android:clipToPadding="false"/> - + android:padding="32dp" + android:visibility="gone"> + + + + + - \ No newline at end of file + + + diff --git a/core/app/src/main/res/layout/fragment_remotes.xml b/core/app/src/main/res/layout/fragment_remotes.xml index c45b249483..eb61fe92fa 100644 --- a/core/app/src/main/res/layout/fragment_remotes.xml +++ b/core/app/src/main/res/layout/fragment_remotes.xml @@ -21,20 +21,23 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:padding="12dp"> + android:padding="16dp"> + android:layout_weight="1" + android:text="Remotes" + android:textSize="18sp" + android:textStyle="bold" + android:textColor="?android:textColorPrimary"/> - + + + + + + + + - - - - - - - + android:layout_weight="1" + android:layout_marginStart="4dp" + android:minWidth="0dp" + android:text="Fetch" + app:icon="@drawable/ic_sync" + app:iconGravity="textStart" + style="@style/Widget.Material3.Button.TonalButton"/> + - + android:visibility="gone"> + + + + + @@ -118,4 +153,10 @@ android:text="Add Remote" app:elevation="0dp" app:icon="@drawable/ic_add"/> - \ No newline at end of file + + + diff --git a/core/app/src/main/res/layout/item_branch.xml b/core/app/src/main/res/layout/item_branch.xml index 3e80e5c1f7..2685a8bc8a 100644 --- a/core/app/src/main/res/layout/item_branch.xml +++ b/core/app/src/main/res/layout/item_branch.xml @@ -6,41 +6,62 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginVertical="6dp" - app:cardElevation="2dp" + app:cardElevation="0dp" app:cardCornerRadius="12dp"> + android:orientation="horizontal" + android:gravity="center_vertical" + android:padding="16dp"> - + android:layout_weight="1" + android:orientation="vertical"> + + + + + + android:textColor="?attr/colorError" + style="@style/Widget.Material3.Button.TextButton"/> - \ No newline at end of file + diff --git a/core/app/src/main/res/layout/item_commit.xml b/core/app/src/main/res/layout/item_commit.xml index 6a54b125c1..b91cb4741a 100644 --- a/core/app/src/main/res/layout/item_commit.xml +++ b/core/app/src/main/res/layout/item_commit.xml @@ -18,6 +18,8 @@ android:id="@+id/textCommitMessage" android:layout_width="match_parent" android:layout_height="wrap_content" + android:maxLines="2" + android:ellipsize="end" android:text="Initial commit" android:textSize="16sp" android:textStyle="bold" @@ -27,7 +29,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:layout_marginTop="8dp"> + android:gravity="center_vertical" + android:layout_marginTop="10dp"> - - + android:layout_marginTop="6dp" + android:text="Jan 01, 2024 12:00" + android:textSize="12sp" + android:textColor="?android:textColorSecondary"/> - \ No newline at end of file + diff --git a/core/app/src/main/res/layout/item_file_change.xml b/core/app/src/main/res/layout/item_file_change.xml index 4589ad85f9..2ae8f89ac0 100644 --- a/core/app/src/main/res/layout/item_file_change.xml +++ b/core/app/src/main/res/layout/item_file_change.xml @@ -48,35 +48,33 @@ + android:layout_marginTop="12dp" + android:orientation="horizontal" + android:gravity="center_vertical"> + android:textColor="?attr/colorError" + style="@style/Widget.Material3.Button.TextButton"/> - \ No newline at end of file + diff --git a/core/app/src/main/res/layout/item_remote.xml b/core/app/src/main/res/layout/item_remote.xml index ad1b08c111..ead4ea199b 100644 --- a/core/app/src/main/res/layout/item_remote.xml +++ b/core/app/src/main/res/layout/item_remote.xml @@ -25,8 +25,10 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:singleLine="true" + android:ellipsize="end" android:text="origin" - android:textSize="18sp" + android:textSize="16sp" android:textStyle="bold" android:textColor="?android:textColorPrimary"/> @@ -34,9 +36,10 @@ android:id="@+id/buttonRemove" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="8dp" android:text="Remove" - android:textSize="12sp" - style="@style/Widget.Material3.Button.OutlinedButton"/> + android:textColor="?attr/colorError" + style="@style/Widget.Material3.Button.TextButton"/> Date: Sun, 27 Sep 2026 15:54:20 +0300 Subject: [PATCH 18/23] fix(ui): resolve colorError from appcompat for destructive dialogs The material library does not expose R.attr.colorError; this project resolves the theme's error color through androidx.appcompat (see MainFragment). XML references to ?attr/colorError were already resolvable, only the Kotlin lookup needed the appcompat constant. --- .../main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt b/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt index fef18ebc85..d633cb5cfc 100644 --- a/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt +++ b/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt @@ -20,7 +20,6 @@ import android.content.res.ColorStateList import android.util.TypedValue import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat -import com.google.android.material.R /** * Presentation helpers for Git dialogs. @@ -38,7 +37,7 @@ import com.google.android.material.R */ fun AlertDialog.styleAsDestructive() { val typedValue = TypedValue() - if (!context.theme.resolveAttribute(R.attr.colorError, typedValue, true)) return + if (!context.theme.resolveAttribute(androidx.appcompat.R.attr.colorError, typedValue, true)) return val colorStateList = if (typedValue.resourceId != 0) { ContextCompat.getColorStateList(context, typedValue.resourceId) From df0c1b3a64f51613efd56579af53219023245281 Mon Sep 17 00:00:00 2001 From: xeriomy Date: Sun, 27 Sep 2026 18:39:38 +0300 Subject: [PATCH 19/23] ui(git): balance the remotes actions and compact the branch list Push, Pull and Fetch become three equal-width action cells (icon above label, one shared height, radius and gap) so the row no longer clips its labels in the half-width panel and stays balanced when the panel is expanded. Branch rows lose their card and their redundant current-branch capsule: the checked out branch is now marked with a leading dot, bold primary-colored text and no actions, every other row offers Checkout and Delete through an overflow menu so the branch name keeps the width it needs. Names stay verbatim and are middle-ellipsized only when they do not fit. --- .../tom/rv2ide/adapters/BranchesAdapter.kt | 71 ++++-- .../main/res/drawable/branch_current_dot.xml | 9 + .../res/drawable/git_action_background.xml | 12 + .../src/main/res/drawable/ic_more_vert.xml | 11 + .../src/main/res/layout/fragment_branches.xml | 88 +++---- .../src/main/res/layout/fragment_remotes.xml | 217 ++++++++++++------ core/app/src/main/res/layout/item_branch.xml | 102 ++++---- .../src/main/res/menu/branch_row_actions.xml | 15 ++ 8 files changed, 334 insertions(+), 191 deletions(-) create mode 100644 core/app/src/main/res/drawable/branch_current_dot.xml create mode 100644 core/app/src/main/res/drawable/git_action_background.xml create mode 100644 core/app/src/main/res/drawable/ic_more_vert.xml create mode 100644 core/app/src/main/res/menu/branch_row_actions.xml diff --git a/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt b/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt index 1636d1fb43..6059728ef9 100644 --- a/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt +++ b/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt @@ -16,6 +16,9 @@ */ package com.tom.rv2ide.adapters +import android.content.res.ColorStateList +import android.graphics.Typeface +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -23,9 +26,9 @@ import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.menu.PopupMenu import com.tom.rv2ide.R import com.tom.rv2ide.databinding.ItemBranchBinding -import android.util.TypedValue /** * @author Mohammed-baqer-null @ https://github.com/Mohammed-baqer-null @@ -56,34 +59,58 @@ class BranchesAdapter( fun bind(branch: String) { val isCurrentBranch = branch == getCurrentBranch() + val view = binding.root + // Branch names are shown verbatim - the UI never parses or rewrites them. binding.textBranchName.text = branch - binding.textCurrentBadge.visibility = if (isCurrentBranch) View.VISIBLE else View.GONE + binding.textBranchName.setTypeface(null, if (isCurrentBranch) Typeface.BOLD else Typeface.NORMAL) + binding.textBranchName.setTextColor( + resolveThemeColor(view, if (isCurrentBranch) R.attr.colorPrimary else R.attr.colorOnSurface) + ?: binding.textBranchName.currentTextColors.defaultColor + ) - if (isCurrentBranch) { - val typedValue = TypedValue() - binding.root.context.theme.resolveAttribute( - android.R.attr.colorPrimary, - typedValue, - true - ) - binding.cardBranch.strokeColor = typedValue.data - binding.cardBranch.strokeWidth = 4 - } else { - binding.cardBranch.strokeWidth = 0 + // A dot marks the checked out branch; other rows keep the same + // space reserved so every branch name lines up in one column. + binding.viewCurrentDot.visibility = if (isCurrentBranch) View.VISIBLE else View.INVISIBLE + resolveThemeColor(view, R.attr.colorPrimary)?.let { color -> + binding.viewCurrentDot.backgroundTintList = ColorStateList.valueOf(color) } - binding.buttonCheckout.setOnClickListener { - if (!isCurrentBranch) { - onCheckoutClick(branch) + // Nothing to check out or delete on the branch that is already + // checked out, so that row stays a clean single line. + binding.buttonMore.visibility = if (isCurrentBranch) View.GONE else View.VISIBLE + + binding.buttonMore.setOnClickListener { anchor -> + val popup = PopupMenu(view.context, anchor) + popup.menuInflater.inflate(R.menu.branch_row_actions, popup.menu) + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_checkout -> { + if (!isCurrentBranch) { + onCheckoutClick(branch) + } + true + } + R.id.action_delete -> { + onDeleteClick(branch) + true + } + else -> false + } } + popup.show() } - - binding.buttonDelete.setOnClickListener { - onDeleteClick(branch) + } + + private fun resolveThemeColor(view: View, attr: Int): Int? { + val context = view.context + val typedValue = TypedValue() + if (!context.theme.resolveAttribute(attr, typedValue, true)) return null + return if (typedValue.resourceId != 0) { + ContextCompat.getColor(context, typedValue.resourceId) + } else { + typedValue.data } - - binding.buttonCheckout.isEnabled = !isCurrentBranch } } @@ -96,4 +123,4 @@ class BranchesAdapter( return oldItem == newItem } } -} \ No newline at end of file +} diff --git a/core/app/src/main/res/drawable/branch_current_dot.xml b/core/app/src/main/res/drawable/branch_current_dot.xml new file mode 100644 index 0000000000..33f59e0f52 --- /dev/null +++ b/core/app/src/main/res/drawable/branch_current_dot.xml @@ -0,0 +1,9 @@ + + + + diff --git a/core/app/src/main/res/drawable/git_action_background.xml b/core/app/src/main/res/drawable/git_action_background.xml new file mode 100644 index 0000000000..59d7b19c8a --- /dev/null +++ b/core/app/src/main/res/drawable/git_action_background.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/core/app/src/main/res/drawable/ic_more_vert.xml b/core/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000000..3da799a337 --- /dev/null +++ b/core/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,11 @@ + + + + diff --git a/core/app/src/main/res/layout/fragment_branches.xml b/core/app/src/main/res/layout/fragment_branches.xml index d822e56c00..b91a346323 100644 --- a/core/app/src/main/res/layout/fragment_branches.xml +++ b/core/app/src/main/res/layout/fragment_branches.xml @@ -10,54 +10,64 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="14dp"> + - - - - - - - - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + android:minWidth="0dp" + android:insetTop="0dp" + android:insetBottom="0dp" + android:paddingHorizontal="10dp" + android:contentDescription="Refresh" + app:icon="@drawable/ic_sync" + app:iconSize="20dp" + app:iconPadding="0dp" + app:iconTint="?android:textColorSecondary"/> - + + + + + - + - + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="14dp"> + - - - - - - - - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + - + android:minWidth="0dp" + android:insetTop="0dp" + android:insetBottom="0dp" + android:paddingHorizontal="10dp" + android:contentDescription="Refresh" + app:icon="@drawable/ic_sync" + app:iconSize="20dp" + app:iconPadding="0dp" + app:iconTint="?android:textColorSecondary"/> + + + + + + + - - + + + + - - + + + + + + + - - + + + + + + + + android:textSize="12sp" + android:textColor="?attr/colorOnSecondaryContainer"/> - + + + - + - + android:orientation="vertical"> + - - + + + + - - - - - - - - - + android:singleLine="true" + android:ellipsize="middle" + android:text="main" + android:textSize="15sp" + android:textColor="?android:textColorPrimary"/> + + - + + + diff --git a/core/app/src/main/res/menu/branch_row_actions.xml b/core/app/src/main/res/menu/branch_row_actions.xml new file mode 100644 index 0000000000..fa5e50e788 --- /dev/null +++ b/core/app/src/main/res/menu/branch_row_actions.xml @@ -0,0 +1,15 @@ + + + + + From db092c328d1c421fe838bb5ad6ad9be7e3f9258f Mon Sep 17 00:00:00 2001 From: xeriomy Date: Sun, 27 Sep 2026 18:40:48 +0300 Subject: [PATCH 20/23] feat(ui): add expand and collapse for the Git side panel The side panel stays the default. A new action in the Git repo header widens the navigation view that already hosts this fragment to the full window and collapses it back to its wrap-content width, so the fragment, the pager and the selected tab keep their state and no new navigation or state container is introduced. The panel collapses itself when another sidebar panel takes over, when the init screen is shown and when the view is destroyed. --- .../fragments/sidebar/GitClientFragment.kt | 57 +++++++++++++++++++ .../main/res/drawable/ic_collapse_panel.xml | 11 ++++ .../src/main/res/drawable/ic_expand_panel.xml | 11 ++++ .../main/res/layout/fragment_git_client.xml | 15 +++++ 4 files changed, 94 insertions(+) create mode 100644 core/app/src/main/res/drawable/ic_collapse_panel.xml create mode 100644 core/app/src/main/res/drawable/ic_expand_panel.xml diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt index 962e9ff81e..6ae1214dbc 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewpager2.adapter.FragmentStateAdapter @@ -25,6 +26,9 @@ class GitClientFragment : Fragment() { private var hasInitialized = false + /** Whether the Git panel currently takes the whole window instead of the side panel. */ + private var isPanelExpanded = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -71,6 +75,56 @@ class GitClientFragment : Fragment() { binding.textRepoBranch.visibility = View.VISIBLE } } + + binding.buttonExpand.setOnClickListener { + setPanelExpanded(!isPanelExpanded) + } + updateExpandButton() + } + + /** + * Expands or collapses the Git side panel. + * + * The panel is the navigation view that hosts this fragment inside the + * editor drawer. Expanding only widens that existing view to the full + * window and collapses it back to its normal wrap-content width, so the + * fragment, the pager and the selected tab all keep their state - no new + * navigation or state container is introduced. + */ + private fun setPanelExpanded(expand: Boolean) { + if (expand == isPanelExpanded) return + + val drawer = activity?.findViewById(R.id.editor_drawerLayout) ?: return + val navView = drawer.findViewById(R.id.startNav) ?: return + val params = navView.layoutParams as? DrawerLayout.LayoutParams ?: return + + isPanelExpanded = expand + params.width = + if (expand) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT + navView.layoutParams = params + drawer.requestLayout() + + updateExpandButton() + } + + private fun updateExpandButton() { + if (_binding == null) return + binding.buttonExpand.setImageResource( + if (isPanelExpanded) R.drawable.ic_collapse_panel else R.drawable.ic_expand_panel + ) + val label = if (isPanelExpanded) "Collapse Git panel" else "Expand Git panel" + binding.buttonExpand.contentDescription = label + binding.buttonExpand.tooltipText = label + } + + /** Restores the normal side panel width, e.g. when another sidebar panel takes over. */ + private fun collapsePanelIfExpanded() { + if (isPanelExpanded) setPanelExpanded(false) + } + + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + if (hidden) collapsePanelIfExpanded() } private fun setupObservers() { @@ -101,6 +155,7 @@ class GitClientFragment : Fragment() { } private fun showInitScreen() { + collapsePanelIfExpanded() binding.repoHeader.visibility = View.GONE binding.navigationRail.visibility = View.GONE binding.viewPager.visibility = View.GONE @@ -179,6 +234,8 @@ class GitClientFragment : Fragment() { } override fun onDestroyView() { + // Never leave the editor drawer full width behind. + collapsePanelIfExpanded() super.onDestroyView() _binding = null } diff --git a/core/app/src/main/res/drawable/ic_collapse_panel.xml b/core/app/src/main/res/drawable/ic_collapse_panel.xml new file mode 100644 index 0000000000..5e33bd9d24 --- /dev/null +++ b/core/app/src/main/res/drawable/ic_collapse_panel.xml @@ -0,0 +1,11 @@ + + + + diff --git a/core/app/src/main/res/drawable/ic_expand_panel.xml b/core/app/src/main/res/drawable/ic_expand_panel.xml new file mode 100644 index 0000000000..ef82dbf241 --- /dev/null +++ b/core/app/src/main/res/drawable/ic_expand_panel.xml @@ -0,0 +1,11 @@ + + + + diff --git a/core/app/src/main/res/layout/fragment_git_client.xml b/core/app/src/main/res/layout/fragment_git_client.xml index ccbfb9c7d7..354c2ba53e 100644 --- a/core/app/src/main/res/layout/fragment_git_client.xml +++ b/core/app/src/main/res/layout/fragment_git_client.xml @@ -43,12 +43,27 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" + android:maxWidth="170dp" + android:maxLines="1" + android:ellipsize="middle" android:paddingHorizontal="10dp" android:paddingVertical="3dp" android:background="@drawable/chip_background" android:textSize="12sp" android:textStyle="bold" android:visibility="gone"/> + + + Date: Sun, 27 Sep 2026 18:41:23 +0300 Subject: [PATCH 21/23] ui(git): unify git feedback, headers and destructive dialogs Screen headers become plain, compact blocks instead of cards - title plus an icon-only refresh, subtitle, optional action and a divider - which gives the narrow panel back the space the card margins used to eat; list items are tightened by a couple of dp. All Git messages now go through showGitFeedback(): success is tinted with the primary container, neutral information keeps the default snackbar look and failures use the error color and stay on screen longer. Precondition notices that used to open a modal dialog are snackbars now, and nothing is ever suppressed. Destructive confirmations keep their title, short explanation and Cancel /action pair, with a readable on-error label on the filled confirm button; clearing saved credentials is styled like the other destructive dialogs. --- .../tom/rv2ide/fragments/BranchesFragment.kt | 10 +- .../tom/rv2ide/fragments/ChangesFragment.kt | 8 +- .../com/tom/rv2ide/fragments/InitFragment.kt | 5 +- .../tom/rv2ide/fragments/RemotesFragment.kt | 24 ++-- .../tom/rv2ide/fragments/SettingsFragment.kt | 13 +- .../rv2ide/utils/DestructiveDialogStyle.kt | 27 +++- .../java/com/tom/rv2ide/utils/GitFeedback.kt | 115 ++++++++++++++++++ .../src/main/res/layout/fragment_changes.xml | 104 +++++++++------- .../src/main/res/layout/fragment_history.xml | 88 ++++++++------ core/app/src/main/res/layout/item_commit.xml | 2 +- .../src/main/res/layout/item_file_change.xml | 2 +- core/app/src/main/res/layout/item_remote.xml | 2 +- 12 files changed, 273 insertions(+), 127 deletions(-) create mode 100644 core/app/src/main/java/com/tom/rv2ide/utils/GitFeedback.kt diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt index bf120d8697..66f2e09eea 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt @@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.tom.rv2ide.R import com.tom.rv2ide.adapters.BranchesAdapter @@ -103,7 +102,10 @@ class BranchesFragment : Fragment() { } viewModel.operationResult.observe(viewLifecycleOwner) { result -> - Snackbar.make(binding.root, result.message, Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback( + result.message, + if (result.success) GitFeedbackStyle.SUCCESS else GitFeedbackStyle.ERROR + ) if (result.success) { viewModel.refreshBranches() } @@ -132,7 +134,7 @@ class BranchesFragment : Fragment() { if (branchName.isNotBlank()) { viewModel.createBranch(branchName) } else { - Snackbar.make(binding.root, "Branch name cannot be empty", Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback("Branch name cannot be empty", GitFeedbackStyle.ERROR) } } .setNegativeButton("Cancel", null) @@ -141,7 +143,7 @@ class BranchesFragment : Fragment() { private fun showDeleteConfirmation(branch: String) { if (branch == currentBranch) { - Snackbar.make(binding.root, "Cannot delete current branch", Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback("Cannot delete current branch", GitFeedbackStyle.ERROR) return } diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt index d1dc4f041b..bfc9f75713 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/ChangesFragment.kt @@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.tom.rv2ide.R import com.tom.rv2ide.adapters.FileChangesAdapter @@ -67,7 +66,10 @@ class ChangesFragment : Fragment() { binding.fabCommit.visibility = if (stagedCount > 0) View.VISIBLE else View.GONE } viewModel.operationResult.observe(viewLifecycleOwner) { result -> - Snackbar.make(binding.root, result.message, Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback( + result.message, + if (result.success) GitFeedbackStyle.SUCCESS else GitFeedbackStyle.ERROR + ) } viewModel.progressMessage.observe(viewLifecycleOwner) { message -> @@ -94,7 +96,7 @@ class ChangesFragment : Fragment() { val email = prefsManager.getGitUserEmail() viewModel.commit(message, author, email) } else { - Snackbar.make(binding.root, "Commit message cannot be empty", Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback("Commit message cannot be empty", GitFeedbackStyle.ERROR) } } .setNegativeButton("Cancel", null) diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/InitFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/InitFragment.kt index eb6cd950bb..ed5fd1ec2b 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/InitFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/InitFragment.kt @@ -23,7 +23,6 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.tom.rv2ide.R import com.tom.rv2ide.configurations.GCProperties @@ -84,7 +83,7 @@ class InitFragment : Fragment() { // Repository is ready, MainActivity will handle navigation } RepositoryStatus.ERROR -> { - Snackbar.make(binding.root, "Failed to initialize repository", Snackbar.LENGTH_LONG).show() + binding.root.showGitFeedback("Failed to initialize repository", GitFeedbackStyle.ERROR) } else -> {} } @@ -174,7 +173,7 @@ class InitFragment : Fragment() { if (url.isNotBlank()) { viewModel.cloneRepository(url, GCProperties.userProject, username, password) } else { - Snackbar.make(binding.root, "URL cannot be empty", Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback("URL cannot be empty", GitFeedbackStyle.ERROR) } } .setNegativeButton("Cancel", null) diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt index 7b7b2cf37c..9105ecaadc 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt @@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.tom.rv2ide.R import com.tom.rv2ide.adapters.RemotesAdapter @@ -115,11 +114,10 @@ class RemotesFragment : Fragment() { } private fun showResult(success: Boolean, message: String) { - Snackbar.make( - binding.root, + binding.root.showGitFeedback( message, - if (success) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG - ).show() + if (success) GitFeedbackStyle.SUCCESS else GitFeedbackStyle.ERROR + ) } private fun setupButtons() { @@ -144,14 +142,6 @@ class RemotesFragment : Fragment() { } } - private fun showErrorDialog(title: String, message: String) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(title) - .setMessage(message) - .setPositiveButton("OK", null) - .show() - } - private fun showAddRemoteDialog() { val dialogView = layoutInflater.inflate(R.layout.dialog_add_remote, null) val editTextName = dialogView.findViewById(R.id.editTextRemoteName) @@ -171,7 +161,7 @@ class RemotesFragment : Fragment() { if (name.isNotBlank() && url.isNotBlank()) { viewModel.addRemote(name, url) } else { - showErrorDialog("Invalid Input", "Name and URL cannot be empty") + binding.root.showGitFeedback("Name and URL cannot be empty", GitFeedbackStyle.ERROR) } } .setNegativeButton("Cancel", null) @@ -181,7 +171,7 @@ class RemotesFragment : Fragment() { private fun showPushDialog() { val remotesList = adapter.currentList if (remotesList.isEmpty()) { - showErrorDialog("No Remotes", "No remotes configured. Add a remote first.") + binding.root.showGitFeedback("No remotes configured. Add a remote first.", GitFeedbackStyle.ERROR) return } @@ -222,7 +212,7 @@ class RemotesFragment : Fragment() { private fun showPullDialog() { val remotesList = adapter.currentList if (remotesList.isEmpty()) { - showErrorDialog("No Remotes", "No remotes configured. Add a remote first.") + binding.root.showGitFeedback("No remotes configured. Add a remote first.", GitFeedbackStyle.ERROR) return } @@ -262,7 +252,7 @@ class RemotesFragment : Fragment() { private fun showFetchDialog() { val remotesList = adapter.currentList if (remotesList.isEmpty()) { - showErrorDialog("No Remotes", "No remotes configured. Add a remote first.") + binding.root.showGitFeedback("No remotes configured. Add a remote first.", GitFeedbackStyle.ERROR) return } diff --git a/core/app/src/main/java/com/tom/rv2ide/fragments/SettingsFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/SettingsFragment.kt index 859ed1f72f..9f658ea3cd 100644 --- a/core/app/src/main/java/com/tom/rv2ide/fragments/SettingsFragment.kt +++ b/core/app/src/main/java/com/tom/rv2ide/fragments/SettingsFragment.kt @@ -23,11 +23,13 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText import com.tom.rv2ide.R import com.tom.rv2ide.databinding.FragmentSettingsBinding +import com.tom.rv2ide.utils.GitFeedbackStyle import com.tom.rv2ide.utils.PreferencesManager +import com.tom.rv2ide.utils.showGitFeedback +import com.tom.rv2ide.utils.styleAsDestructive import com.tom.rv2ide.viewmodel.GitViewModel /** @@ -84,14 +86,15 @@ class SettingsFragment : Fragment() { binding.buttonClearCredentials.setOnClickListener { MaterialAlertDialogBuilder(requireContext()) .setTitle("Clear Credentials") - .setMessage("Are you sure you want to clear saved credentials?") + .setMessage("Remove the username and password saved on this device?") .setPositiveButton("Clear") { _, _ -> prefsManager.clearCredentials() binding.textCredentialsStatus.text = "Not saved" - Snackbar.make(binding.root, "Credentials cleared", Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback("Credentials cleared", GitFeedbackStyle.SUCCESS) } .setNegativeButton("Cancel", null) .show() + .styleAsDestructive() } } @@ -115,9 +118,9 @@ class SettingsFragment : Fragment() { prefsManager.setGitUserEmail(email) viewModel.setUserConfig(name, email) loadSettings() - Snackbar.make(binding.root, "User config updated", Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback("User config updated", GitFeedbackStyle.SUCCESS) } else { - Snackbar.make(binding.root, "Name and email cannot be empty", Snackbar.LENGTH_SHORT).show() + binding.root.showGitFeedback("Name and email cannot be empty", GitFeedbackStyle.ERROR) } } .setNegativeButton("Cancel", null) diff --git a/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt b/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt index d633cb5cfc..8ecad97ce4 100644 --- a/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt +++ b/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt @@ -31,19 +31,34 @@ import androidx.core.content.ContextCompat */ /** - * Colors the confirm button of a destructive dialog (delete branch, remove - * remote, discard changes) with the theme's error color so that it cannot be - * mistaken for a routine action. + * Styles a destructive confirmation dialog (delete branch, discard changes, + * remove remote, clear credentials): the confirm button is filled with the + * theme's error color and its label switches to the matching on-error color so + * the action cannot be mistaken for a routine one while staying readable. + * + * The dialog itself, its title, its message and its callbacks are untouched. */ fun AlertDialog.styleAsDestructive() { val typedValue = TypedValue() if (!context.theme.resolveAttribute(androidx.appcompat.R.attr.colorError, typedValue, true)) return - + val colorStateList = if (typedValue.resourceId != 0) { ContextCompat.getColorStateList(context, typedValue.resourceId) } else { ColorStateList.valueOf(typedValue.data) } - - colorStateList?.let { getButton(AlertDialog.BUTTON_POSITIVE).backgroundTintList = it } + + val confirmButton = getButton(AlertDialog.BUTTON_POSITIVE) + colorStateList?.let { confirmButton.backgroundTintList = it } + + // Keep the label legible on top of the error-colored background. + val onError = TypedValue() + if (context.theme.resolveAttribute(com.tom.rv2ide.R.attr.colorOnError, onError, true)) { + val color = if (onError.resourceId != 0) { + ContextCompat.getColor(context, onError.resourceId) + } else { + onError.data + } + confirmButton.setTextColor(color) + } } diff --git a/core/app/src/main/java/com/tom/rv2ide/utils/GitFeedback.kt b/core/app/src/main/java/com/tom/rv2ide/utils/GitFeedback.kt new file mode 100644 index 0000000000..7129e8f7c5 --- /dev/null +++ b/core/app/src/main/java/com/tom/rv2ide/utils/GitFeedback.kt @@ -0,0 +1,115 @@ +/* + * This file is part of AndroidCodeStudio. + * + * AndroidCodeStudio is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidCodeStudio is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidCodeStudio. If not, see . +*/ +package com.tom.rv2ide.utils + +import android.content.Context +import android.content.res.ColorStateList +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.annotation.AttrRes +import androidx.core.content.ContextCompat +import com.tom.rv2ide.R +import com.google.android.material.R as MaterialR +import com.google.android.material.snackbar.Snackbar + +/** + * Presentation helpers for Git feedback. + * + * The Git screens surface every operation result through a Snackbar so that the + * user always sees the outcome of an action. These helpers only change how a + * message looks - what is reported, when it is reported and how errors are + * handled further up the stack stays exactly the same. + * + * @author Mohammed-baqer-null @ https://github.com/Mohammed-baqer-null + */ + +/** How a feedback message should be presented. */ +enum class GitFeedbackStyle { + /** A finished operation that succeeded. */ + SUCCESS, + + /** Neutral, non urgent information. */ + INFO, + + /** A failed or refused operation - shown longer and in the error color. */ + ERROR, +} + +/** + * Shows [message] on the closest Snackbar with the styling of [style]. + * + * Success uses the theme's primary container, errors use the theme's error + * color (and stay on screen longer), everything else keeps the default + * neutral Snackbar appearance. Errors are never hidden or turned into a modal + * dialog. + */ +fun View.showGitFeedback( + message: String, + style: GitFeedbackStyle = GitFeedbackStyle.INFO, +) { + val snackbar = + Snackbar.make( + this, + message, + if (style == GitFeedbackStyle.ERROR) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT, + ) + + val colors = + when (style) { + GitFeedbackStyle.SUCCESS -> + pairOf(context, R.attr.colorPrimaryContainer, R.attr.colorOnPrimaryContainer) + GitFeedbackStyle.ERROR -> pairOf(context, R.attr.colorError, R.attr.colorOnError) + GitFeedbackStyle.INFO -> null + } + + if (colors != null) { + snackbar.view.backgroundTintList = ColorStateList.valueOf(colors.first) + findMessageView(snackbar.view)?.setTextColor(colors.second) + } + + snackbar.show() +} + +/** The message TextView of a Snackbar - its label always has to stay readable. */ +private fun findMessageView(view: View): TextView? { + if (view is TextView && view !is Button) return view + if (view is ViewGroup) { + for (index in 0 until view.childCount) { + findMessageView(view.getChildAt(index))?.let { return it } + } + } + return view.findViewById(MaterialR.id.snackbar_text) +} + +private fun pairOf(context: Context, @AttrRes background: Int, @AttrRes foreground: Int): Pair? { + val bg = resolveThemeColor(context, background) ?: return null + val fg = resolveThemeColor(context, foreground) ?: return null + return bg to fg +} + +private fun resolveThemeColor(context: Context, @AttrRes attr: Int): Int? { + val typedValue = TypedValue() + if (!context.theme.resolveAttribute(attr, typedValue, true)) return null + return if (typedValue.resourceId != 0) { + ContextCompat.getColor(context, typedValue.resourceId) + } else { + typedValue.data + } +} diff --git a/core/app/src/main/res/layout/fragment_changes.xml b/core/app/src/main/res/layout/fragment_changes.xml index abcc571756..3417c288d0 100644 --- a/core/app/src/main/res/layout/fragment_changes.xml +++ b/core/app/src/main/res/layout/fragment_changes.xml @@ -10,63 +10,73 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="14dp"> + - - - - - - - - + android:orientation="horizontal" + android:gravity="center_vertical"> + - + android:layout_weight="1" + android:text="Changes" + android:textSize="18sp" + android:textStyle="bold" + android:textColor="?android:textColorPrimary"/> + + android:minWidth="0dp" + android:insetTop="0dp" + android:insetBottom="0dp" + android:paddingHorizontal="10dp" + android:contentDescription="Refresh" + app:icon="@drawable/ic_sync" + app:iconSize="20dp" + app:iconPadding="0dp" + app:iconTint="?android:textColorSecondary"/> - + + + + + + + - + - + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="14dp"> + - - - - - - - - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + android:minWidth="0dp" + android:insetTop="0dp" + android:insetBottom="0dp" + android:paddingHorizontal="10dp" + android:contentDescription="Refresh" + app:icon="@drawable/ic_sync" + app:iconSize="20dp" + app:iconPadding="0dp" + app:iconTint="?android:textColorSecondary"/> - + + + + + diff --git a/core/app/src/main/res/layout/item_file_change.xml b/core/app/src/main/res/layout/item_file_change.xml index 2ae8f89ac0..5858ef3645 100644 --- a/core/app/src/main/res/layout/item_file_change.xml +++ b/core/app/src/main/res/layout/item_file_change.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginVertical="6dp" + android:layout_marginVertical="4dp" app:cardElevation="0dp" app:cardCornerRadius="12dp"> diff --git a/core/app/src/main/res/layout/item_remote.xml b/core/app/src/main/res/layout/item_remote.xml index ead4ea199b..5d4b702c62 100644 --- a/core/app/src/main/res/layout/item_remote.xml +++ b/core/app/src/main/res/layout/item_remote.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginVertical="6dp" + android:layout_marginVertical="4dp" app:cardElevation="0dp" app:cardCornerRadius="12dp"> From 24fbd662c94d01d560680be4ad7c03eaf13ea2c1 Mon Sep 17 00:00:00 2001 From: xeriomy Date: Sun, 27 Sep 2026 18:52:10 +0300 Subject: [PATCH 22/23] fix(ui): use the framework popup menu for branch row actions Material 1.13.0 no longer ships com.google.android.material.menu.PopupMenu, so the branch actions menu has to come from the platform, and the fallback text color used TextView.currentTextColor. --- .../main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt b/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt index 6059728ef9..5ff7a92302 100644 --- a/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt +++ b/core/app/src/main/java/com/tom/rv2ide/adapters/BranchesAdapter.kt @@ -22,11 +22,11 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.menu.PopupMenu import com.tom.rv2ide.R import com.tom.rv2ide.databinding.ItemBranchBinding @@ -66,7 +66,7 @@ class BranchesAdapter( binding.textBranchName.setTypeface(null, if (isCurrentBranch) Typeface.BOLD else Typeface.NORMAL) binding.textBranchName.setTextColor( resolveThemeColor(view, if (isCurrentBranch) R.attr.colorPrimary else R.attr.colorOnSurface) - ?: binding.textBranchName.currentTextColors.defaultColor + ?: binding.textBranchName.currentTextColor ) // A dot marks the checked out branch; other rows keep the same @@ -82,7 +82,7 @@ class BranchesAdapter( binding.buttonMore.setOnClickListener { anchor -> val popup = PopupMenu(view.context, anchor) - popup.menuInflater.inflate(R.menu.branch_row_actions, popup.menu) + popup.inflate(R.menu.branch_row_actions) popup.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.action_checkout -> { From 8b47f65201e9cb30a0acf16010989467e3c426af Mon Sep 17 00:00:00 2001 From: xeriomy Date: Sun, 27 Sep 2026 20:31:20 +0300 Subject: [PATCH 23/23] fix(ui): prevent outer git client scrolling --- .../res/layout/fragment_editor_sidebar.xml | 21 +++++-------- .../res/layout/layout_editor_file_tree.xml | 30 ++++++++++++------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/core/app/src/main/res/layout/fragment_editor_sidebar.xml b/core/app/src/main/res/layout/fragment_editor_sidebar.xml index 2ce856bdf9..9ccf903405 100644 --- a/core/app/src/main/res/layout/fragment_editor_sidebar.xml +++ b/core/app/src/main/res/layout/fragment_editor_sidebar.xml @@ -34,22 +34,17 @@ android:visibility="gone" /> - - + - - - - - + app:defaultNavHost="false" /> diff --git a/core/app/src/main/res/layout/layout_editor_file_tree.xml b/core/app/src/main/res/layout/layout_editor_file_tree.xml index 77d9e04d03..9c28cd930a 100644 --- a/core/app/src/main/res/layout/layout_editor_file_tree.xml +++ b/core/app/src/main/res/layout/layout_editor_file_tree.xml @@ -44,17 +44,25 @@ - - - + + + + + + + \ No newline at end of file