Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
DataStore.initGlobal()
addPreferencesFromResource(R.xml.global_preferences)

val appTheme = findPreference<ColorPickerPreference>(Key.APP_THEME)!!
val appTheme = findPreference<ThemePickerPreference>(Key.APP_THEME)!!
val nightTheme = findPreference<SimpleMenuPreference>(Key.NIGHT_THEME)!!
appTheme.setOnPreferenceChangeListener { _, newTheme ->
if (DataStore.serviceState.started) {
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ object Theme {

private fun defaultTheme() = PINK_SSR

/**
* Metadata for a theme shown in the modern named picker.
*
* @param id one of the Theme int constants above (persisted to appTheme)
* @param nameRes display name string resource
* @param previewColor color resource for the preview swatch shown next to the name
*/
data class ThemeInfo(val id: Int, val nameRes: Int, val previewColor: Int)

/**
* Modern, full-fledged M3 themes presented by name in the picker dialog.
* The legacy single-accent palettes stay behind the "Classic colors…" grid.
* Order here is the display order in the dialog.
*/
val MODERN_THEMES: List<ThemeInfo> = listOf(
ThemeInfo(DYNAMIC, R.string.theme_dynamic, R.color.color_dynamic_swatch),
)

fun apply(context: Context) {
context.setTheme(getTheme())
}
Expand Down
118 changes: 0 additions & 118 deletions app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt

This file was deleted.

230 changes: 230 additions & 0 deletions app/src/main/java/moe/matsuri/nb4a/ui/ThemePickerPreference.kt
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
}
}

Comment thread
greptile-apps[bot] marked this conversation as resolved.
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(
Comment thread
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)
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<string name="menu_group">Group</string>
<string name="menu_about">About</string>
<string name="theme">Theme</string>
<string name="theme_dynamic">Dynamic (Material You)</string>
<string name="theme_classic_colors">Classic colors…</string>
<string name="theme_classic_color_swatch">Color theme %1$d</string>
<string name="document">Document</string>
<string name="group_default">Ungrouped</string>
<string name="quick_toggle">Toggle</string>
Expand Down
Loading
Loading