diff --git a/.github/workflows/asm_build.yml b/.github/workflows/asm_build.yml index ee2efda313..29139d9f88 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,19 +52,19 @@ 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 /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 - 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@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@v4 + 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 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() diff --git a/core/app/build.gradle.kts b/core/app/build.gradle.kts index be200a7f62..ed936425f4 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 } } @@ -74,6 +86,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 +126,11 @@ android { buildTypes { debug { - signingConfig = signingConfigs.getByName("custom") + // 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") } release { @@ -302,3 +335,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") + } +} 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/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/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/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..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 @@ -16,15 +16,19 @@ */ 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 +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.tom.rv2ide.R import com.tom.rv2ide.databinding.ItemBranchBinding -import android.util.TypedValue /** * @author Mohammed-baqer-null @ https://github.com/Mohammed-baqer-null @@ -55,33 +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.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.currentTextColor + ) - 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.inflate(R.menu.branch_row_actions) + 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 } } @@ -94,4 +123,4 @@ class BranchesAdapter( return oldItem == newItem } } -} \ No newline at end of file +} 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/BranchesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/BranchesFragment.kt index 105f84ce8c..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 @@ -89,16 +88,24 @@ 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() } 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() } @@ -127,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) @@ -136,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 } @@ -148,6 +155,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..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 @@ -49,14 +48,28 @@ 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() + binding.root.showGitFeedback( + result.message, + if (result.success) GitFeedbackStyle.SUCCESS else GitFeedbackStyle.ERROR + ) } viewModel.progressMessage.observe(viewLifecycleOwner) { message -> @@ -83,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) @@ -149,6 +162,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/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/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/MainFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/MainFragment.kt index 9604f1f0be..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 @@ -22,13 +22,17 @@ 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 +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 @@ -106,60 +110,94 @@ 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 - - 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 + 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 } + } + } - 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 + 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( + 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 { + 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 +216,38 @@ 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) + sheetView + .findViewById(R.id.btnCloseProjects) + ?.setOnClickListener { bottomSheet.dismiss() } + + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + + val projectDirs = collectProjectDirs() var adapter: ProjectsListAdapter? = null @@ -207,38 +267,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) @@ -284,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, + androidx.appcompat.R.attr.borderlessButtonStyle, + ) + .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, + androidx.appcompat.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) { @@ -770,6 +844,39 @@ 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 recentBadgeContainer: View = view.findViewById(R.id.recentBadgeContainer) + 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.recentBadgeContainer.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, @@ -781,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 } @@ -798,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/java/com/tom/rv2ide/fragments/RemotesFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/RemotesFragment.kt index 1899326d75..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 @@ -83,24 +82,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 +112,13 @@ class RemotesFragment : Fragment() { } } } + + private fun showResult(success: Boolean, message: String) { + binding.root.showGitFeedback( + message, + if (success) GitFeedbackStyle.SUCCESS else GitFeedbackStyle.ERROR + ) + } private fun setupButtons() { binding.fabAddRemote.setOnClickListener { @@ -134,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) @@ -161,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) @@ -171,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 } @@ -212,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 } @@ -252,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 } @@ -299,6 +299,7 @@ class RemotesFragment : Fragment() { } .setNegativeButton("Cancel", null) .show() + .styleAsDestructive() } override fun onDestroyView() { 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/fragments/sidebar/GitClientFragment.kt b/core/app/src/main/java/com/tom/rv2ide/fragments/sidebar/GitClientFragment.kt index a10a2e4341..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?, @@ -37,6 +41,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 +59,74 @@ 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 + } + } + + 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() { viewModel.isRepositoryInitialized.observe(viewLifecycleOwner) { initialized -> if (initialized && !hasInitialized) { @@ -81,6 +155,8 @@ class GitClientFragment : Fragment() { } private fun showInitScreen() { + collapsePanelIfExpanded() + binding.repoHeader.visibility = View.GONE binding.navigationRail.visibility = View.GONE binding.viewPager.visibility = View.GONE @@ -93,6 +169,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 @@ -157,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/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 { 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..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 @@ -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 @@ -241,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 @@ -461,10 +465,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 +472,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 +523,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/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..8ecad97ce4 --- /dev/null +++ b/core/app/src/main/java/com/tom/rv2ide/utils/DestructiveDialogStyle.kt @@ -0,0 +1,64 @@ +/* + * 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 + +/** + * 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 + */ + +/** + * 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) + } + + 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/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_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/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-land/fragment_main.xml b/core/app/src/main/res/layout-land/fragment_main.xml index 5bbadbf234..2dc614b410 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,157 @@ ~ 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..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,37 +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" /> @@ -39,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" @@ -69,7 +85,24 @@ 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" /> + android:visibility="gone" + tools:visibility="visible" /> + + + - \ No newline at end of file + 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..a159a66635 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" /> + 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"> + + @@ -98,11 +131,19 @@ android:layout_height="wrap_content" /> + + @@ -117,8 +158,8 @@ @@ -130,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" /> - - @@ -192,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" /> @@ -204,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/app/src/main/res/layout/fragment_branches.xml b/core/app/src/main/res/layout/fragment_branches.xml index 07fc51eb8f..b91a346323 100644 --- a/core/app/src/main/res/layout/fragment_branches.xml +++ b/core/app/src/main/res/layout/fragment_branches.xml @@ -10,37 +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: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"/> - + + + + + + + + + + + + - \ 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..3417c288d0 100644 --- a/core/app/src/main/res/layout/fragment_changes.xml +++ b/core/app/src/main/res/layout/fragment_changes.xml @@ -10,53 +10,73 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - - - + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="14dp"> + - - + + - + android:layout_weight="1" + android:text="Changes" + android:textSize="18sp" + android:textStyle="bold" + android:textColor="?android:textColorPrimary"/> + - + app:iconSize="20dp" + app:iconPadding="0dp" + app:iconTint="?android:textColorSecondary"/> - - + + + + + + + - + android:padding="32dp" + android:visibility="gone"> + + + + + - - \ No newline at end of file + + + + + 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/fragment_git_client.xml b/core/app/src/main/res/layout/fragment_git_client.xml index 90ad9a64dc..354c2ba53e 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,83 @@ + 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..5aa754214f 100644 --- a/core/app/src/main/res/layout/fragment_history.xml +++ b/core/app/src/main/res/layout/fragment_history.xml @@ -10,36 +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: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:padding="32dp" + android:visibility="gone"> + + + + + - \ No newline at end of file + + + diff --git a/core/app/src/main/res/layout/fragment_main.xml b/core/app/src/main/res/layout/fragment_main.xml index 8ce92b4f3c..f022f7e199 100644 --- a/core/app/src/main/res/layout/fragment_main.xml +++ b/core/app/src/main/res/layout/fragment_main.xml @@ -1,59 +1,150 @@ - - - - - - - - - - - \ No newline at end of file + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="16dp" + android:paddingBottom="24dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/app/src/main/res/layout/fragment_remotes.xml b/core/app/src/main/res/layout/fragment_remotes.xml index c45b249483..3ad3b95a05 100644 --- a/core/app/src/main/res/layout/fragment_remotes.xml +++ b/core/app/src/main/res/layout/fragment_remotes.xml @@ -10,95 +10,196 @@ android:layout_height="match_parent" android:orientation="vertical"> - + - + android:orientation="vertical" + android:paddingHorizontal="16dp" + android:paddingTop="14dp"> + - - + + + + + + + + + + + + - + android:gravity="center" + android:background="@drawable/git_action_background" + android:foreground="?attr/selectableItemBackground" + android:clipToOutline="true" + android:clickable="true" + android:focusable="true"> + + + - - + + + + + + + + android:layout_marginTop="4dp" + android:maxLines="1" + android:text="Pull" + android:textSize="12sp" + android:textColor="?attr/colorOnSecondaryContainer"/> - - + + + + - - - - - - - + android:layout_marginTop="4dp" + android:maxLines="1" + android:text="Fetch" + android:textSize="12sp" + android:textColor="?attr/colorOnSecondaryContainer"/> + - + + + - + android:visibility="gone"> + + + + + @@ -118,4 +220,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..a1e6dcf43c 100644 --- a/core/app/src/main/res/layout/item_branch.xml +++ b/core/app/src/main/res/layout/item_branch.xml @@ -1,46 +1,59 @@ - - + - + android:orientation="vertical"> + - + android:minHeight="48dp" + android:orientation="horizontal" + android:gravity="center_vertical" + android:paddingStart="14dp" + android:paddingEnd="4dp" + android:paddingVertical="4dp"> + + + - - - - + + - \ 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..9d63858dfb 100644 --- a/core/app/src/main/res/layout/item_commit.xml +++ b/core/app/src/main/res/layout/item_commit.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"> @@ -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..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"> @@ -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_project.xml b/core/app/src/main/res/layout/item_project.xml index 8d9b7fd491..b2c2744830 100644 --- a/core/app/src/main/res/layout/item_project.xml +++ b/core/app/src/main/res/layout/item_project.xml @@ -1,9 +1,14 @@ + android:orientation="horizontal" + android:gravity="center_vertical" + android:padding="12dp"> + + + + + + + android:layout_weight="1" + android:layout_marginStart="12dp" + android:layout_marginEnd="4dp" + android:orientation="vertical"> + android:ellipsize="end" + android:singleLine="true" + tools:text="MyProject" /> + + + + + + + tools:text="Recent" /> - - - + - \ 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..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"> @@ -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"/> - - - + + + + + + + \ No newline at end of file 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 8f8983d0cf..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,34 +1,35 @@ + - - + android:textAlignment="textStart" + android:textColor="?attr/colorOnSurfaceVariant" + app:cornerRadius="12dp" + app:iconPadding="12dp" + app:iconSize="20dp" + app:iconTint="?attr/colorOnSurfaceVariant" + app:rippleColor="?attr/colorSecondaryContainer" + tools:icon="@drawable/ic_settings" + tools:text="@string/msg_preferences" /> + 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..e8ebc42f32 --- /dev/null +++ b/core/app/src/main/res/layout/preference_header.xml @@ -0,0 +1,19 @@ + + + + + + + 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 @@ + + + + + diff --git a/core/resources/src/main/res/values/strings.xml b/core/resources/src/main/res/values/strings.xml index b56eec7f6d..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 @@ -189,11 +191,20 @@ 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. 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. @@ -238,7 +249,14 @@ 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 diff --git a/signing/debug.keystore b/signing/debug.keystore new file mode 100644 index 0000000000..d6aa4cac9f Binary files /dev/null and b/signing/debug.keystore differ 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" }