forked from starifly/NekoBoxForAndroid
-
Notifications
You must be signed in to change notification settings - Fork 0
Two-tier theme picker + Material 3 dark themes (Dark High Contrast, Dracula, Nord, Monokai, Ayu, Catppuccin) #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
762083f
Add two-tier theme picker (modern named list + Classic colors grid)
hawkff fa9a271
Harden theme picker: null-safe widget cast + accessibility
hawkff 37661e1
Theme picker: scrollable modern list + robust swatch binding
hawkff 3bd3854
Add Material 3 Dark High Contrast theme (OLED black)
hawkff 67636f3
Dark High Contrast: M3 black chrome + visible connected FAB icon
hawkff 99d6c69
Add Dracula-M3, Nord, Monokai, Ayu, Catppuccin themes; fix appbar crash
hawkff 85048c7
CR fixes: official Catppuccin Mocha colors + DHC dialog fabConnectedC…
hawkff 1386524
Theme polish: Dracula-M3 yellow names, DHC muted status text, FAB inset
hawkff 1a1b976
BottomAppBar variants: inherit base style explicitly (CR)
hawkff a5d9213
DHC green accent, Legacy colors rename, progress dialog padding
hawkff cdf6a51
Complete M3 migration: convert remaining AppCompat/M2 widgets
hawkff a197494
Clarify inset comment terminology (CR): 'bottom bar' not 'status bar'
hawkff 5c08261
DHC ringed swatch + list bottom padding to clear docked FAB/stats
hawkff f18ad9a
Increase list bottom padding to 150dp to fully clear docked FAB/stats
hawkff 1656477
Hide FAB on scroll + 168dp list padding so it never overlaps cards
hawkff File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 0 additions & 118 deletions
118
app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt
This file was deleted.
Oops, something went wrong.
230 changes: 230 additions & 0 deletions
230
app/src/main/java/moe/matsuri/nb4a/ui/ThemePickerPreference.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| package moe.matsuri.nb4a.ui | ||
|
|
||
| import android.content.Context | ||
| import android.content.res.Resources | ||
| import android.graphics.drawable.Drawable | ||
| import android.util.AttributeSet | ||
| import android.view.Gravity | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import android.widget.GridLayout | ||
| import android.widget.ImageView | ||
| import android.widget.LinearLayout | ||
| import android.widget.TextView | ||
| import androidx.appcompat.app.AlertDialog | ||
| import androidx.core.content.res.ResourcesCompat | ||
| import androidx.core.content.res.TypedArrayUtils | ||
| import androidx.core.graphics.drawable.DrawableCompat | ||
| import androidx.core.view.setPadding | ||
| import androidx.preference.Preference | ||
| import androidx.preference.PreferenceViewHolder | ||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| import io.nekohasekai.sagernet.R | ||
| import io.nekohasekai.sagernet.ktx.getColorAttr | ||
| import io.nekohasekai.sagernet.utils.Theme | ||
| import kotlin.math.roundToInt | ||
|
|
||
| /** | ||
| * Two-tier theme picker. | ||
| * | ||
| * The preference opens a modern named list of full-fledged Material 3 themes | ||
| * (each row: preview swatch + name). A trailing "Classic colors…" row opens the | ||
| * legacy single-accent color grid (the original swatch-grid UX). | ||
| * | ||
| * Both paths persist the same int [Theme] id via [persistInt] so the rest of the | ||
| * app (Theme.getTheme / DataStore.appTheme) is unchanged. | ||
| */ | ||
| class ThemePickerPreference | ||
| @JvmOverloads constructor( | ||
| context: Context, attrs: AttributeSet? = null, defStyle: Int = TypedArrayUtils.getAttr( | ||
| context, | ||
| androidx.preference.R.attr.editTextPreferenceStyle, | ||
| android.R.attr.editTextPreferenceStyle | ||
| ) | ||
| ) : Preference( | ||
| context, attrs, defStyle | ||
| ) { | ||
|
|
||
| private var inited = false | ||
|
|
||
| override fun onBindViewHolder(holder: PreferenceViewHolder) { | ||
| super.onBindViewHolder(holder) | ||
|
|
||
| val widgetFrame = holder.findViewById(android.R.id.widget_frame) as? LinearLayout | ||
| ?: return | ||
|
|
||
| if (!inited) { | ||
| inited = true | ||
| widgetFrame.addView( | ||
| nekoImageView(context.getColorAttr(R.attr.colorPrimary), 48, 0) | ||
| ) | ||
| widgetFrame.visibility = View.VISIBLE | ||
| } | ||
| } | ||
|
|
||
| private fun nekoImageView(color: Int, sizeDp: Int, paddingDp: Int): ImageView { | ||
| val factor = context.resources.displayMetrics.density | ||
| val size = (sizeDp * factor).roundToInt() | ||
| val paddingSize = (paddingDp * factor).roundToInt() | ||
|
|
||
| return ImageView(context).apply { | ||
| layoutParams = ViewGroup.LayoutParams(size, size) | ||
| setPadding(paddingSize) | ||
| setImageDrawable(nekoAtColor(resources, color)) | ||
| } | ||
| } | ||
|
|
||
| private fun nekoAtColor(res: Resources, color: Int): Drawable { | ||
| val neko = ResourcesCompat.getDrawable( | ||
| res, R.drawable.ic_baseline_fiber_manual_record_24, null | ||
| )!! | ||
| DrawableCompat.setTint(neko.mutate(), color) | ||
| return neko | ||
| } | ||
|
|
||
| private fun dp(value: Int): Int = | ||
| (value * context.resources.displayMetrics.density).roundToInt() | ||
|
|
||
| override fun onClick() { | ||
| super.onClick() | ||
| showModernDialog() | ||
| } | ||
|
|
||
| /** Modern named list: one row per [Theme.MODERN_THEMES] entry + a "Classic colors…" row. */ | ||
| private fun showModernDialog() { | ||
| lateinit var dialog: AlertDialog | ||
|
|
||
| val container = LinearLayout(context).apply { | ||
| orientation = LinearLayout.VERTICAL | ||
| setPadding(dp(8), dp(8), dp(8), dp(8)) | ||
| } | ||
|
|
||
| for (info in Theme.MODERN_THEMES) { | ||
| container.addView( | ||
| buildRow( | ||
| name = context.getString(info.nameRes), | ||
| swatchColor = ResourcesCompat.getColor(context.resources, info.previewColor, context.theme), | ||
| ) { | ||
| select(info.id) | ||
| dialog.dismiss() | ||
| } | ||
| ) | ||
| } | ||
|
|
||
| // Trailing entry: open the legacy single-accent color grid. | ||
| container.addView( | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| buildRow( | ||
| name = context.getString(R.string.theme_classic_colors), | ||
| swatchColor = null, | ||
| icon = R.drawable.ic_baseline_color_lens_24, | ||
| ) { | ||
| dialog.dismiss() | ||
| showClassicGrid() | ||
| } | ||
| ) | ||
|
|
||
| dialog = MaterialAlertDialogBuilder(context) | ||
| .setTitle(title) | ||
| .setView(container) | ||
| .setNegativeButton(android.R.string.cancel, null) | ||
| .show() | ||
| } | ||
|
|
||
| private fun buildRow( | ||
| name: String, | ||
| swatchColor: Int?, | ||
| icon: Int? = null, | ||
| onClick: () -> Unit, | ||
| ): View { | ||
| return LinearLayout(context).apply { | ||
| orientation = LinearLayout.HORIZONTAL | ||
| gravity = Gravity.CENTER_VERTICAL | ||
| isClickable = true | ||
| isFocusable = true | ||
| setPadding(dp(16), dp(14), dp(16), dp(14)) | ||
| // Selectable item background for ripple feedback. | ||
| val outValue = android.util.TypedValue() | ||
| context.theme.resolveAttribute( | ||
| android.R.attr.selectableItemBackground, outValue, true | ||
| ) | ||
| setBackgroundResource(outValue.resourceId) | ||
| // Announce the theme name to screen readers when the row is focused. | ||
| contentDescription = name | ||
|
|
||
| val leading = when { | ||
| swatchColor != null -> nekoImageView(swatchColor, 28, 0) | ||
| icon != null -> ImageView(context).apply { | ||
| layoutParams = ViewGroup.LayoutParams(dp(28), dp(28)) | ||
| setImageDrawable( | ||
| ResourcesCompat.getDrawable(resources, icon, context.theme)?.also { | ||
| DrawableCompat.setTint( | ||
| it.mutate(), context.getColorAttr(android.R.attr.textColorPrimary) | ||
| ) | ||
| } | ||
| ) | ||
| } | ||
| else -> View(context).apply { | ||
| layoutParams = ViewGroup.LayoutParams(dp(28), dp(28)) | ||
| } | ||
| } | ||
| addView(leading) | ||
|
|
||
| addView(TextView(context).apply { | ||
| text = name | ||
| setPadding(dp(16), 0, 0, 0) | ||
| textSize = 16f | ||
| setTextColor(context.getColorAttr(android.R.attr.textColorPrimary)) | ||
| layoutParams = LinearLayout.LayoutParams( | ||
| 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f | ||
| ) | ||
| }) | ||
|
|
||
| setOnClickListener { onClick() } | ||
| } | ||
| } | ||
|
|
||
| /** Legacy single-accent color grid (the original swatch-grid UX). */ | ||
| private fun showClassicGrid() { | ||
| lateinit var dialog: AlertDialog | ||
|
|
||
| val grid = GridLayout(context).apply { | ||
| columnCount = 4 | ||
|
|
||
| val colors = context.resources.getIntArray(R.array.material_colors) | ||
| for ((index, color) in colors.withIndex()) { | ||
| // Swatch index+1 is the Theme id (see Theme.kt). Skip themes that | ||
| // are presented in the modern named list instead of the grid. | ||
| val themeId = index + 1 | ||
| if (Theme.MODERN_THEMES.any { it.id == themeId }) continue | ||
|
|
||
| addView(nekoImageView(color, 64, 0).apply { | ||
| contentDescription = | ||
| context.getString(R.string.theme_classic_color_swatch, themeId) | ||
| setOnClickListener { | ||
| select(themeId) | ||
| dialog.dismiss() | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| dialog = MaterialAlertDialogBuilder(context) | ||
| .setTitle(R.string.theme_classic_colors) | ||
| .setView(LinearLayout(context).apply { | ||
| gravity = Gravity.CENTER | ||
| layoutParams = ViewGroup.LayoutParams( | ||
| ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT | ||
| ) | ||
| addView(grid) | ||
| }) | ||
| .setNegativeButton(android.R.string.cancel, null) | ||
| .show() | ||
| } | ||
|
|
||
| private fun select(themeId: Int) { | ||
| // Match the legacy swatch-grid ordering: persist first, then notify the | ||
| // change listener (SettingsPreferenceFragment applies/recreates). | ||
| persistInt(themeId) | ||
| callChangeListener(themeId) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.