Add thems support (#19)

* Default (#15)

* Create Test Debug Release.yml

* Add files via upload

* Refactor GitHub Actions workflow for debug release (#5)

* Refactor GitHub Actions workflow for debug release

* Update Test Debug Release.yml

* Delete .github/workflows/Test Debug Release.yml

* Add files via upload

* added theme suppoet (#11)

* added theme suppoet

* Fix theme preset mode handling and picker filtering (#12)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Add files via upload

* Update app/src/main/res/values/theme_presets.xml

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Persist theme preset settings before refreshing the UI (#13)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Atomically persist light and dark theme presets (#14)

* Add files via upload

* Add files via upload

* Update AccentAdapter.kt

* Delete test-debug-release.yml

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Use black text on Rena and Pixel primary colors (#16)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Add files via upload (#18)

* Add files via upload

* Add files via upload

* adding finishing touch

* rebase

* fix1

* fix2

* Add files via upload

* Add files via upload

* Update .github/workflows/test-debug-release.yml

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Adjust Ukrrooter theme surface color (#20)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

Add curated theme presets with automatic light and dark switching

What Changed
Users can enable 12 full-color theme presets instead of separately choosing an accent, light/dark mode, and contrast setting.
Users can apply one fixed preset or choose separate light and dark presets that follow the system appearance.
Theme and accent choices now open visual card pickers, with changes applied immediately and saved for future launches.
Existing accent, light/dark, and high-contrast controls are hidden while presets are enabled.
Preset selection now keeps light and dark choices together, and dark-mode handling no longer overrides automatic preset selection.
Added coverage for light-mode, dark-mode, automatic switching, and fixed dark preset behavior.
Added a workflow for producing architecture-specific debug builds for internal testing.
pull/1360/head
Sumon Kayal 1 week ago committed by GitHub
parent 4918fb28d0
commit 66fab61bac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,241 @@
name: "Test Debug Release"
on:
workflow_dispatch:
inputs:
architecture:
description: 'APK architecture to upload for manual test builds'
required: true
type: choice
options:
- arm64-v8a
- armeabi-v7a
- x86
- x86_64
- universal
default: universal
push:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build-debug:
name: Build test debug APK
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up JDK 21
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: 'temurin'
java-version: '21'
cache: 'gradle'
verify-signature: true
- name: Read latest dated version from CHANGELOG.md
id: changelog
shell: bash
run: |
set -euo pipefail
if [[ ! -f CHANGELOG.md ]]; then
echo "::error::CHANGELOG.md not found"
exit 1
fi
# Expected format (this repo's changelog uses a blockquoted heading):
# > # 1.8.9.1 (2026-06)
CHANGELOG_LINE="$(
grep -m1 -E '^>[[:space:]]+#[[:space:]]+[^[:space:]]+[[:space:]]+\([0-9]{4}-[0-9]{2}\)[[:space:]]*$' CHANGELOG.md
)" || {
echo "::error::No dated version heading was found in CHANGELOG.md"
echo "::error::Expected: > # 1.8.9.1 (YYYY-MM)"
exit 1
}
VERSION="$(sed -E 's/^>[[:space:]]+#[[:space:]]+([^[:space:]]+).*$/\1/' <<< "$CHANGELOG_LINE")"
RELEASE_DATE="$(sed -E 's/^>[[:space:]]+#[[:space:]]+[^[:space:]]+[[:space:]]+\(([0-9]{4}-[0-9]{2})\).*$/\1/' <<< "$CHANGELOG_LINE")"
# Extract the complete latest dated release section, excluding
# any older releases. Stops at the next top-level version heading.
RELEASE_SECTION="$(
awk -v heading="$CHANGELOG_LINE" '
$0 == heading { found=1; print; next }
found && /^>[[:space:]]+#[[:space:]]+/ { exit }
found { print }
' CHANGELOG.md
)"
# Keep the values safe for filenames/tags.
SAFE_VERSION="$(tr -cd '[:alnum:]._-v' <<< "$VERSION")"
if [[ -z "$SAFE_VERSION" || -z "$RELEASE_DATE" || -z "$RELEASE_SECTION" ]]; then
echo "::error::Could not extract the latest dated release section from CHANGELOG.md"
exit 1
fi
{
echo "version=$SAFE_VERSION"
echo "date=$RELEASE_DATE"
echo "heading=$CHANGELOG_LINE"
echo "section<<CHANGELOG_EOF"
echo "$RELEASE_SECTION"
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"
echo "Latest dated changelog version: $SAFE_VERSION ($RELEASE_DATE)"
- name: Generate debug keystore
run: |
if [[ -f app/debug.keystore ]]; then
echo "Using the committed app/debug.keystore — signature stays the same across runs."
else
echo "::warning::app/debug.keystore not found in the repo — generating a temporary one for this run only. Commit the keystore this workflow uploads as a build artifact to app/debug.keystore so future test builds share one signature and can be installed as updates instead of reinstalls."
keytool -genkeypair -v \
-keystore app/debug.keystore \
-storepass android \
-keypass android \
-alias androiddebugkey \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-dname "CN=Android Debug,O=Android,C=US"
fi
# app/build.gradle only defines signingConfigs.debug when this
# file exists and is non-empty — without it, the keystore above
# is never actually referenced and the build silently falls
# back to Gradle's own implicit debug signing config instead.
cat > keystore.properties <<EOF
signingConfig.storeFile=debug.keystore
signingConfig.storePassword=android
signingConfig.keyAlias=androiddebugkey
signingConfig.keyPassword=android
EOF
- name: Create local.properties
run: touch local.properties
- name: Build foss debug APK
run: |
chmod +x ./gradlew
./gradlew assembleFossDebug --stacktrace --no-daemon
- name: Locate and rename APK
id: apk
shell: bash
env:
VERSION: ${{ steps.changelog.outputs.version }}
RELEASE_DATE: ${{ steps.changelog.outputs.date }}
TARGET_ARCH: ${{ github.event_name == 'workflow_dispatch' && inputs.architecture || 'universal' }}
run: |
set -euo pipefail
mkdir -p dist
shopt -s nullglob
SHORT_SHA="${GITHUB_SHA::7}"
# Scoped to the foss flavor only (assembleFossDebug above), so
# this looks directly in that flavor's debug output directory.
APK_PATHS=(app/build/outputs/apk/foss/debug/*.apk)
if (( ${#APK_PATHS[@]} == 0 )); then
echo "::error::No debug APKs were produced"
exit 1
fi
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
echo "dir=dist" >> "$GITHUB_OUTPUT"
SELECTED_APK=""
SELECTED_ABI=""
SELECTED_FLAVOR=""
for APK_PATH in "${APK_PATHS[@]}"; do
# Flavor is the directory two levels above the APK file
# (.../apk/<flavor>/debug/<file>.apk).
FLAVOR="$(basename "$(dirname "$(dirname "$APK_PATH")")")"
# Pull the ABI out of the filename by matching a known ABI
# token instead of assuming the default "app-<abi>-debug"
# naming — this project's APKs are named "YTDLnis-...".
ABI="$(basename "$APK_PATH" .apk | grep -oE 'arm64-v8a|armeabi-v7a|x86_64|x86|universal' || true)"
if [[ -z "$ABI" ]]; then
ABI="$(basename "$APK_PATH" .apk)"
fi
if [[ "$ABI" == "$TARGET_ARCH" ]]; then
SELECTED_APK="$APK_PATH"
SELECTED_ABI="$ABI"
SELECTED_FLAVOR="$FLAVOR"
break
fi
done
if [[ -z "$SELECTED_APK" ]]; then
echo "::error::No APK found for requested architecture: $TARGET_ARCH"
echo "::error::Produced APKs:"
printf ' %s\n' "${APK_PATHS[@]}"
exit 1
fi
OUTPUT_APK="dist/YTDLnis-debug-v${VERSION}-${RELEASE_DATE}-${SHORT_SHA}-${SELECTED_FLAVOR}-${SELECTED_ABI}.apk"
cp "$SELECTED_APK" "$OUTPUT_APK"
echo "architecture=$SELECTED_ABI" >> "$GITHUB_OUTPUT"
echo "apk=$OUTPUT_APK" >> "$GITHUB_OUTPUT"
echo "Selected architecture: $SELECTED_ABI"
echo "Selected APK: $SELECTED_APK"
- name: Create internal debug release notes
id: notes
shell: bash
env:
VERSION: ${{ steps.changelog.outputs.version }}
RELEASE_DATE: ${{ steps.changelog.outputs.date }}
CHANGELOG_HEADING: ${{ steps.changelog.outputs.heading }}
CHANGELOG_SECTION: ${{ steps.changelog.outputs.section }}
SHORT_SHA: ${{ steps.apk.outputs.short_sha }}
ARCHITECTURE: ${{ steps.apk.outputs.architecture }}
run: |
cat > debug-release-notes.md <<EOF
## YTDLnis internal debug build
**Changelog version:** ${VERSION}
**Changelog date:** ${RELEASE_DATE}
**Commit:** ${GITHUB_SHA}
**Short commit:** ${SHORT_SHA}
**Architecture:** ${ARCHITECTURE}
This is an **internal testing build** generated manually from the repository.
- It is a debug build.
- It is a prerelease.
- It does **not** use the production release tag.
- The changelog version/date are used for identification only.
- Install only for internal testing.
### Latest dated CHANGELOG.md entry
${CHANGELOG_SECTION}
EOF
- name: Upload as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: YTDLnis-debug-foss-v${{ steps.changelog.outputs.version }}-${{ steps.apk.outputs.short_sha }}-${{ steps.apk.outputs.architecture }}
path: |
${{ steps.apk.outputs.dir }}/*.apk
debug-release-notes.md
retention-days: 1

@ -0,0 +1,105 @@
package com.deniscerri.ytdl.util
import android.app.Activity
import android.graphics.Color
import android.util.TypedValue
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.material.R as MaterialR
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ThemeUtilInstrumentedTest {
private val context
get() = InstrumentationRegistry.getInstrumentation().targetContext
@Before
fun setUp() {
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
@After
fun tearDown() {
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit()
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
@Test
fun updateTheme_automaticPresetResetsLegacyDarkModeAndUsesLightPreset() {
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putBoolean("use_theme_presets", true)
.putBoolean("theme_preset_auto_mode", true)
.putString("theme_preset_light_id", ThemeUtil.ThemePreset.Classic.value)
.putString("theme_preset_dark_id", ThemeUtil.ThemePreset.Dark.value)
.putString("ytdlnis_theme", "Dark")
.commit()
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
ActivityScenario.launch(ThemeUtilTestActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
ThemeUtil.updateTheme(activity)
assertThat(AppCompatDelegate.getDefaultNightMode())
.isEqualTo(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
assertThat(activity.themeColor(MaterialR.attr.colorPrimary))
.isEqualTo(Color.parseColor("#639eff"))
}
}
}
@Test
fun updateTheme_automaticPresetUsesDarkPresetInNightMode() {
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putBoolean("use_theme_presets", true)
.putBoolean("theme_preset_auto_mode", true)
.putString("theme_preset_light_id", ThemeUtil.ThemePreset.Classic.value)
.putString("theme_preset_dark_id", ThemeUtil.ThemePreset.Dark.value)
.commit()
ActivityScenario.launch(ThemeUtilDarkTestActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
ThemeUtil.updateTheme(activity)
assertThat(AppCompatDelegate.getDefaultNightMode())
.isEqualTo(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
assertThat(activity.themeColor(MaterialR.attr.colorSurface))
.isEqualTo(Color.parseColor("#131313"))
}
}
}
@Test
fun updateTheme_darkConcretePresetEnablesNightModeAndAppliesPreset() {
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putBoolean("use_theme_presets", true)
.putBoolean("theme_preset_auto_mode", false)
.putString("theme_preset_concrete_id", ThemeUtil.ThemePreset.Dark.value)
.commit()
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
ActivityScenario.launch(ThemeUtilTestActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
ThemeUtil.updateTheme(activity)
assertThat(AppCompatDelegate.getDefaultNightMode())
.isEqualTo(AppCompatDelegate.MODE_NIGHT_YES)
assertThat(activity.themeColor(MaterialR.attr.colorSurface))
.isEqualTo(Color.parseColor("#131313"))
}
}
}
}
private fun Activity.themeColor(attribute: Int): Int {
val value = TypedValue()
theme.resolveAttribute(attribute, value, true)
return value.data
}

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".util.ThemeUtilTestActivity"
android:exported="false" />
<activity
android:name=".util.ThemeUtilDarkTestActivity"
android:exported="false" />
</application>
</manifest>

@ -0,0 +1,15 @@
package com.deniscerri.ytdl.util
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
class ThemeUtilDarkTestActivity : Activity() {
override fun attachBaseContext(newBase: Context) {
val darkConfiguration = Configuration(newBase.resources.configuration).apply {
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or
Configuration.UI_MODE_NIGHT_YES
}
super.attachBaseContext(newBase.createConfigurationContext(darkConfiguration))
}
}

@ -0,0 +1,15 @@
package com.deniscerri.ytdl.util
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
class ThemeUtilTestActivity : Activity() {
override fun attachBaseContext(newBase: Context) {
val lightConfiguration = Configuration(newBase.resources.configuration).apply {
uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or
Configuration.UI_MODE_NIGHT_NO
}
super.attachBaseContext(newBase.createConfigurationContext(lightConfiguration))
}
}

@ -0,0 +1,71 @@
package com.deniscerri.ytdl.ui.adapter
import android.content.Context
import android.content.res.ColorStateList
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.ItemAccentBinding
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.util.ThemeUtil
import com.google.android.material.color.DynamicColors
import com.google.android.material.R as MaterialR
class AccentAdapter(val host: SettingHost) : RecyclerView.Adapter<AccentAdapter.AccentViewHolder>() {
class AccentViewHolder(val binding: ItemAccentBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccentViewHolder {
val binding = ItemAccentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return AccentViewHolder(binding)
}
override fun getItemCount() = ThemeUtil.availableAccents.size
override fun onBindViewHolder(holder: AccentViewHolder, position: Int) {
val accent = ThemeUtil.availableAccents[position]
val context = holder.binding.root.context
// "Default" is Material You. Only wrap it with real dynamic color when
// the device actually supports it — wrapContextIfAvailable() returns the
// context unchanged (not BaseTheme) when it doesn't, which would preview
// whatever accent happens to currently be active instead of a neutral one.
val themedContext = when {
accent.value == "Default" && DynamicColors.isDynamicColorAvailable() ->
DynamicColors.wrapContextIfAvailable(context)
accent.value == "Default" ->
ContextThemeWrapper(context, R.style.BaseTheme)
else ->
ContextThemeWrapper(context, accent.styleResource)
}
val primary = themedContext.colorFromAttr(MaterialR.attr.colorPrimary)
val onPrimary = themedContext.colorFromAttr(MaterialR.attr.colorOnPrimary)
holder.binding.apply {
accentName.text = root.context.getString(accent.nameResource)
accentCard.setCardBackgroundColor(primary)
accentCard.strokeColor = onPrimary
accentName.setTextColor(onPrimary)
accentCard.rippleColor = ColorStateList.valueOf(onPrimary).withAlpha(40)
root.setOnClickListener {
val preferences = PreferenceManager.getDefaultSharedPreferences(host.getHostContext())
preferences.edit { putString("theme_accent", accent.value) }
ThemeUtil.updateThemes()
host.refreshUI()
}
}
}
}
private fun Context.colorFromAttr(attr: Int): Int {
val value = TypedValue()
theme.resolveAttribute(attr, value, true)
return value.data
}

@ -0,0 +1,84 @@
package com.deniscerri.ytdl.ui.adapter
import android.content.Context
import android.content.res.ColorStateList
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.databinding.ItemThemePresetBinding
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.util.ThemeUtil
import com.google.android.material.R as MaterialR
/**
* Grid adapter for the 12 ported theme presets. Two modes, mirroring the split
* BAI has between ThemeAdapter/ThemeSelectionDialogFragment's MODE_APPLY and
* MODE_CHOOSE:
* - [Mode.APPLY]: tapping a card saves it as the concrete preset immediately
* and recreates. Used by the top-level "Theme preset" picker.
* - [Mode.CHOOSE]: tapping a card just reports it back through [onChosen]
* instead of persisting anything. Used when this adapter is hosted inside
* the light/dark pairing dialog, which decides whether the pick belongs in
* the light slot or the dark slot.
*
* Each card previews its preset the same way [AccentAdapter] does: wrap the
* preset's style in a [ContextThemeWrapper] and read colorPrimary/colorOnPrimary
* back off it.
*/
class ThemePresetAdapter(
private val host: SettingHost?,
private val mode: Mode,
private val presets: List<ThemeUtil.ThemePreset> = ThemeUtil.availableThemePresets,
private val onChosen: ((ThemeUtil.ThemePreset) -> Unit)? = null
) : RecyclerView.Adapter<ThemePresetAdapter.ThemePresetViewHolder>() {
enum class Mode { APPLY, CHOOSE }
class ThemePresetViewHolder(
val binding: ItemThemePresetBinding
) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThemePresetViewHolder {
val binding = ItemThemePresetBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ThemePresetViewHolder(binding)
}
override fun getItemCount() = presets.size
override fun onBindViewHolder(holder: ThemePresetViewHolder, position: Int) {
val preset = presets[position]
val context = holder.binding.root.context
val themedContext = ContextThemeWrapper(context, preset.styleResource)
val primary = themedContext.colorFromAttr(MaterialR.attr.colorPrimary)
val onPrimary = themedContext.colorFromAttr(MaterialR.attr.colorOnPrimary)
holder.binding.apply {
presetName.text = context.getString(preset.nameResource)
presetCard.setCardBackgroundColor(primary)
presetCard.strokeColor = onPrimary
presetName.setTextColor(onPrimary)
presetCard.rippleColor = ColorStateList.valueOf(onPrimary).withAlpha(40)
root.setOnClickListener {
when (mode) {
Mode.APPLY -> {
val activity = host?.getHostContext() ?: return@setOnClickListener
ThemeUtil.setConcreteThemePreset(activity, preset)
ThemeUtil.updateThemes()
host.refreshUI()
}
Mode.CHOOSE -> onChosen?.invoke(preset)
}
}
}
}
}
private fun Context.colorFromAttr(attr: Int): Int {
val value = TypedValue()
theme.resolveAttribute(attr, value, true)
return value.data
}

@ -2,7 +2,6 @@ package com.deniscerri.ytdl.ui.more
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -24,6 +23,8 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
@ -68,7 +69,10 @@ class MoreFragment : Fragment() {
settings = view.findViewById(R.id.settings)
val appIcon = view.findViewById<ImageView>(R.id.app_icon)
if (mainSharedPreferences.getString("theme_accent", "blue") == "Default" && Build.VERSION.SDK_INT >= 32) {
val usingMaterialYou = mainSharedPreferences.getString("theme_accent", "blue") == "Default" &&
DynamicColors.isDynamicColorAvailable() &&
!ThemeUtil.isThemePresetsEnabled(requireContext())
if (usingMaterialYou) {
appIcon.backgroundTintList = MaterialColors.getColorStateList(requireContext(), R.attr.colorPrimary, ContextCompat.getColorStateList(requireContext(), R.color.icon_fg)!!)
} else {
appIcon.backgroundTintList = null

@ -8,9 +8,12 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
@ -31,13 +34,16 @@ import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.databinding.NavOptionsItemBinding
import com.deniscerri.ytdl.ui.adapter.AccentAdapter
import com.deniscerri.ytdl.ui.adapter.IconsSheetAdapter
import com.deniscerri.ytdl.ui.adapter.NavBarOptionsAdapter
import com.deniscerri.ytdl.ui.adapter.ThemePresetAdapter
import com.deniscerri.ytdl.ui.more.settings.SettingHost
import com.deniscerri.ytdl.ui.more.settings.SettingModule
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -176,6 +182,7 @@ object GeneralSettingsModule : SettingModule {
}
"ytdlnis_theme" -> {
(pref as ListPreference).apply {
isVisible = !preferences.getBoolean("use_theme_presets", false)
summary = entry
setOnPreferenceChangeListener { _, newValue ->
val dialog = MaterialAlertDialogBuilder(host.getHostContext())
@ -236,17 +243,190 @@ object GeneralSettingsModule : SettingModule {
}
}
"theme_accent" -> {
(pref as ListPreference).apply {
summary = entry
setOnPreferenceChangeListener { _, _ ->
pref.apply {
isVisible = !preferences.getBoolean("use_theme_presets", false)
val currentValue = preferences.getString("theme_accent", "blue")
ThemeUtil.availableAccents.firstOrNull { it.value == currentValue }?.let {
summary = context.getString(it.nameResource)
}
setOnPreferenceClickListener {
val bottomSheet = BottomSheetDialog(host.getHostContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.generic_list)
val recycler = bottomSheet.findViewById<RecyclerView>(R.id.download_recyclerview)!!
recycler.layoutManager = GridLayoutManager(context, 2)
recycler.adapter = AccentAdapter(host)
bottomSheet.show()
val displayMetrics = DisplayMetrics()
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
false
}
}
}
"use_theme_presets" -> {
(pref as SwitchPreferenceCompat).apply {
setOnPreferenceChangeListener { _, newValue ->
val enabled = newValue as Boolean
val autoMode = preferences.getBoolean("theme_preset_auto_mode", false)
host.findPref("theme_accent")?.isVisible = !enabled
host.findPref("ytdlnis_theme")?.isVisible = !enabled
host.findPref("high_contrast")?.isVisible = !enabled
host.findPref("theme_preset_auto_mode")?.isVisible = enabled
host.findPref("theme_preset_concrete")?.isVisible = enabled && !autoMode
host.findPref("theme_preset_light_dark")?.isVisible = enabled && autoMode
preferences.edit(commit = true) {
putBoolean(pref.key, enabled)
}
ThemeUtil.updateThemes()
host.refreshUI()
true
}
}
}
"theme_preset_auto_mode" -> {
(pref as SwitchPreferenceCompat).apply {
isVisible = preferences.getBoolean("use_theme_presets", false)
setOnPreferenceChangeListener { _, newValue ->
val autoMode = newValue as Boolean
host.findPref("theme_preset_concrete")?.isVisible = !autoMode
host.findPref("theme_preset_light_dark")?.isVisible = autoMode
preferences.edit(commit = true) {
putBoolean(pref.key, autoMode)
}
ThemeUtil.updateThemes()
host.refreshUI()
true
}
}
}
"theme_preset_concrete" -> {
pref.apply {
isVisible = preferences.getBoolean("use_theme_presets", false) &&
!preferences.getBoolean("theme_preset_auto_mode", false)
summary = context.getString(ThemeUtil.getConcreteThemePreset(context).nameResource)
setOnPreferenceClickListener {
val bottomSheet = BottomSheetDialog(host.getHostContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.generic_list)
val recycler = bottomSheet.findViewById<RecyclerView>(R.id.download_recyclerview)!!
recycler.layoutManager = GridLayoutManager(context, 2)
recycler.adapter = ThemePresetAdapter(host, ThemePresetAdapter.Mode.APPLY)
bottomSheet.show()
val displayMetrics = DisplayMetrics()
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
wm.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
false
}
}
}
"theme_preset_light_dark" -> {
pref.apply {
isVisible = preferences.getBoolean("use_theme_presets", false) &&
preferences.getBoolean("theme_preset_auto_mode", false)
fun updateSummary() {
summary = "${context.getString(ThemeUtil.getLightThemePreset(context).nameResource)} / " +
context.getString(ThemeUtil.getDarkThemePreset(context).nameResource)
}
updateSummary()
setOnPreferenceClickListener {
var stagedLight = ThemeUtil.getLightThemePreset(context)
var stagedDark = ThemeUtil.getDarkThemePreset(context)
val binding = host.getHostContext().layoutInflater.inflate(R.layout.dialog_light_dark_preset, null)
val lightCard = binding.findViewById<MaterialCardView>(R.id.lightSlotCard)
val lightName = binding.findViewById<TextView>(R.id.lightSlotName)
val darkCard = binding.findViewById<MaterialCardView>(R.id.darkSlotCard)
val darkName = binding.findViewById<TextView>(R.id.darkSlotName)
fun renderSlot(card: MaterialCardView, name: TextView, preset: ThemeUtil.ThemePreset) {
val themedContext = ContextThemeWrapper(context, preset.styleResource)
val value = TypedValue()
themedContext.theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, value, true)
val primary = value.data
themedContext.theme.resolveAttribute(com.google.android.material.R.attr.colorOnPrimary, value, true)
val onPrimary = value.data
name.text = context.getString(preset.nameResource)
card.setCardBackgroundColor(primary)
card.strokeColor = onPrimary
name.setTextColor(onPrimary)
}
renderSlot(lightCard, lightName, stagedLight)
renderSlot(darkCard, darkName, stagedDark)
fun openPicker(
presets: List<ThemeUtil.ThemePreset>,
onPicked: (ThemeUtil.ThemePreset) -> Unit
) {
val bottomSheet = BottomSheetDialog(host.getHostContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.generic_list)
val recycler = bottomSheet.findViewById<RecyclerView>(R.id.download_recyclerview)!!
recycler.layoutManager = GridLayoutManager(context, 2)
recycler.adapter = ThemePresetAdapter(null, ThemePresetAdapter.Mode.CHOOSE, presets) { chosen ->
onPicked(chosen)
bottomSheet.dismiss()
}
bottomSheet.show()
}
lightCard.setOnClickListener {
openPicker(ThemeUtil.availableThemePresets.filter { !it.isDark }) { chosen ->
stagedLight = chosen
renderSlot(lightCard, lightName, stagedLight)
}
}
darkCard.setOnClickListener {
openPicker(ThemeUtil.availableThemePresets.filter { it.isDark }) { chosen ->
stagedDark = chosen
renderSlot(darkCard, darkName, stagedDark)
}
}
MaterialAlertDialogBuilder(host.getHostContext())
.setTitle(R.string.theme_preset_choose_title)
.setView(binding)
.setPositiveButton(R.string.ok) { _, _ ->
ThemeUtil.setLightAndDarkThemePresets(context, stagedLight, stagedDark)
updateSummary()
ThemeUtil.updateThemes()
host.refreshUI()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
}
}
"high_contrast" -> {
(pref as SwitchPreferenceCompat).apply {
isVisible = !preferences.getBoolean("use_theme_presets", false)
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.updateThemes()
host.refreshUI()
@ -467,4 +647,4 @@ object GeneralSettingsModule : SettingModule {
}
}
}
}
}

@ -5,12 +5,15 @@ import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import android.text.Spanned
import android.util.TypedValue
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.preference.PreferenceManager
@ -76,6 +79,147 @@ object ThemeUtil {
AppIcon.Green,
)
/**
* One entry per "theme_accent" option. [value] is the exact string stored in
* SharedPreferences (must match res/values/arrays.xml's accents_values and the
* `when` branches in [updateTheme]). [styleResource] is only meaningful for the
* non-Default entries Default is previewed/applied via DynamicColors instead,
* since it has no fixed style of its own.
*/
sealed class AccentOption(
@StringRes val nameResource: Int,
val value: String,
@StyleRes val styleResource: Int
) {
object Default : AccentOption(R.string.material_you, "Default", R.style.BaseTheme)
object Blue : AccentOption(R.string.blue, "blue", R.style.Theme_Blue)
object Red : AccentOption(R.string.red, "red", R.style.Theme_Red)
object Green : AccentOption(R.string.green, "green", R.style.Theme_Green)
object Purple : AccentOption(R.string.purple, "purple", R.style.Theme_Purple)
object Yellow : AccentOption(R.string.yellow, "yellow", R.style.Theme_Yellow)
object Orange : AccentOption(R.string.orange, "orange", R.style.Theme_Orange)
object Monochrome : AccentOption(R.string.monochrome, "monochrome", R.style.Theme_Monochrome)
}
val availableAccents = listOf(
AccentOption.Default,
AccentOption.Blue,
AccentOption.Red,
AccentOption.Green,
AccentOption.Purple,
AccentOption.Yellow,
AccentOption.Orange,
AccentOption.Monochrome,
)
/**
* Ported from BAI's Theme.Mode. CONCRETE = always use one fixed preset.
* AUTO_LIGHT_DARK = separately pick a light-mode preset and a dark-mode
* preset, and switch between them based on the system's current light/dark
* state checked directly via Configuration rather than porting BAI's own
* pre-Android-10 detection/warning path, since ytdlnis's AppCompatDelegate
* usage elsewhere already handles that compatibility range.
*/
enum class ThemePresetMode { CONCRETE, AUTO_LIGHT_DARK }
/**
* Ported from BAI's Theme.java ThemeDescriptor list. [isDark] reflects each
* preset's actual rendered luminance, not always BAI's own stored flag
* see theme_presets.xml's header comment for the one entry ("Ukrrooter")
* where the two disagree.
*/
sealed class ThemePreset(
@StringRes val nameResource: Int,
val value: String,
@StyleRes val styleResource: Int,
val isDark: Boolean
) {
object Classic : ThemePreset(R.string.theme_preset_classic, "classic", R.style.ThemePreset_Classic, false)
object Ruby : ThemePreset(R.string.theme_preset_ruby, "ruby", R.style.ThemePreset_Ruby, true)
object Rena : ThemePreset(R.string.theme_preset_rena, "rena", R.style.ThemePreset_Rena, true)
object Ukrrooter : ThemePreset(R.string.theme_preset_ukrrooter, "ukrrooter", R.style.ThemePreset_Ukrrooter, true)
object Amoled : ThemePreset(R.string.theme_preset_amoled, "amoled", R.style.ThemePreset_Amoled, true)
object Pixel : ThemePreset(R.string.theme_preset_pixel, "pixel", R.style.ThemePreset_Pixel, false)
object FDroidLight : ThemePreset(R.string.theme_preset_fdroid_light, "fdroid_light", R.style.ThemePreset_FDroidLight, false)
object Dark : ThemePreset(R.string.theme_preset_dark, "dark", R.style.ThemePreset_Dark, true)
object Gold : ThemePreset(R.string.theme_preset_gold, "gold", R.style.ThemePreset_Gold, true)
object RenaLight : ThemePreset(R.string.theme_preset_rena_light, "rena_light", R.style.ThemePreset_RenaLight, false)
object Mint : ThemePreset(R.string.theme_preset_mint, "mint", R.style.ThemePreset_Mint, true)
object FDroidDark : ThemePreset(R.string.theme_preset_fdroid_dark, "fdroid_dark", R.style.ThemePreset_FDroidDark, true)
}
val availableThemePresets = listOf(
ThemePreset.Classic,
ThemePreset.Ruby,
ThemePreset.Rena,
ThemePreset.Ukrrooter,
ThemePreset.Amoled,
ThemePreset.Pixel,
ThemePreset.FDroidLight,
ThemePreset.Dark,
ThemePreset.Gold,
ThemePreset.RenaLight,
ThemePreset.Mint,
ThemePreset.FDroidDark,
)
fun isThemePresetsEnabled(context: Context): Boolean =
PreferenceManager.getDefaultSharedPreferences(context).getBoolean("use_theme_presets", false)
fun getThemePresetMode(context: Context): ThemePresetMode {
val auto = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("theme_preset_auto_mode", false)
return if (auto) ThemePresetMode.AUTO_LIGHT_DARK else ThemePresetMode.CONCRETE
}
fun setThemePresetAutoMode(context: Context, auto: Boolean) {
PreferenceManager.getDefaultSharedPreferences(context).edit { putBoolean("theme_preset_auto_mode", auto) }
}
private fun findPreset(value: String?, fallback: ThemePreset): ThemePreset =
availableThemePresets.firstOrNull { it.value == value } ?: fallback
fun getConcreteThemePreset(context: Context): ThemePreset = findPreset(
PreferenceManager.getDefaultSharedPreferences(context).getString("theme_preset_concrete_id", null),
ThemePreset.Classic
)
fun setConcreteThemePreset(context: Context, preset: ThemePreset) {
PreferenceManager.getDefaultSharedPreferences(context).edit { putString("theme_preset_concrete_id", preset.value) }
}
fun getLightThemePreset(context: Context): ThemePreset = findPreset(
PreferenceManager.getDefaultSharedPreferences(context).getString("theme_preset_light_id", null),
ThemePreset.Classic
)
fun getDarkThemePreset(context: Context): ThemePreset = findPreset(
PreferenceManager.getDefaultSharedPreferences(context).getString("theme_preset_dark_id", null),
ThemePreset.Dark
)
/**
* Writes both slots in a single Editor transaction so a process death
* mid-save can't persist one side of the pair without the other.
*/
fun setLightAndDarkThemePresets(context: Context, light: ThemePreset, dark: ThemePreset) {
PreferenceManager.getDefaultSharedPreferences(context).edit {
putString("theme_preset_light_id", light.value)
putString("theme_preset_dark_id", dark.value)
}
}
private fun applyThemePreset(activity: Activity) {
val preset = when (getThemePresetMode(activity)) {
ThemePresetMode.CONCRETE -> getConcreteThemePreset(activity)
ThemePresetMode.AUTO_LIGHT_DARK -> {
val nightMode = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
if (nightMode == Configuration.UI_MODE_NIGHT_YES) getDarkThemePreset(activity) else getLightThemePreset(activity)
}
}
activity.setTheme(preset.styleResource)
}
fun recreateMain() {
activities.firstOrNull { it.javaClass == MainActivity::class.java }?.recreate()
}
@ -96,41 +240,53 @@ object ThemeUtil {
fun updateTheme(activity: Activity) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
//update accent
when (sharedPreferences.getString("theme_accent","blue")) {
"Default" -> {
DynamicColors.applyToActivityIfAvailable(activity)
activity.setTheme(R.style.BaseTheme)
}
"blue" -> activity.setTheme(R.style.Theme_Blue)
"red" -> activity.setTheme(R.style.Theme_Red)
"green" -> activity.setTheme(R.style.Theme_Green)
"purple" -> activity.setTheme(R.style.Theme_Purple)
"yellow" -> activity.setTheme(R.style.Theme_Yellow)
"orange" -> activity.setTheme(R.style.Theme_Orange)
"monochrome" -> activity.setTheme(R.style.Theme_Monochrome)
}
//high contrast theme
if (sharedPreferences.getBoolean("high_contrast",false)) {
activity.theme.applyStyle(R.style.Pure, true)
}
val theme = sharedPreferences.getString("ytdlnis_theme", "System")!!
when (theme) {
"Light" -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
if (isThemePresetsEnabled(activity)) {
val nightMode = when (getThemePresetMode(activity)) {
ThemePresetMode.AUTO_LIGHT_DARK -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
ThemePresetMode.CONCRETE -> if (getConcreteThemePreset(activity).isDark) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
}
"Dark" -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
AppCompatDelegate.setDefaultNightMode(nightMode)
applyThemePreset(activity)
} else {
//update accent
when (sharedPreferences.getString("theme_accent","blue")) {
"Default" -> {
activity.setTheme(R.style.BaseTheme)
DynamicColors.applyToActivityIfAvailable(activity)
}
"blue" -> activity.setTheme(R.style.Theme_Blue)
"red" -> activity.setTheme(R.style.Theme_Red)
"green" -> activity.setTheme(R.style.Theme_Green)
"purple" -> activity.setTheme(R.style.Theme_Purple)
"yellow" -> activity.setTheme(R.style.Theme_Yellow)
"orange" -> activity.setTheme(R.style.Theme_Orange)
"monochrome" -> activity.setTheme(R.style.Theme_Monochrome)
}
// or "System"
else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
//high contrast theme
if (sharedPreferences.getBoolean("high_contrast",false)) {
activity.theme.applyStyle(R.style.Pure, true)
}
}
when (sharedPreferences.getString("ytdlnis_theme", "System")) {
"Light" -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}
"Dark" -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
// or "System"
else -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
}
val theme = sharedPreferences.getString("ytdlnis_theme", "System")!!
val iconMode = sharedPreferences.getString("ytdlnis_icon", "Default")!!
updateAppIcon(activity,theme, iconMode)
}
@ -138,7 +294,7 @@ object ThemeUtil {
fun getThemeColor(context: Context, colorCode: Int): Int {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val accent = sharedPreferences.getString("theme_accent", "blue")
return if (accent == "blue"){
return if (accent == "blue" && !isThemePresetsEnabled(context)){
"d43c3b".toInt(16)
}else{
val value = TypedValue()
@ -217,4 +373,4 @@ object ThemeUtil {
}
}
}
}
}

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/theme_preset_choose_light"
android:textStyle="bold" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/lightSlotCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:strokeWidth="2dp">
<TextView
android:id="@+id/lightSlotName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="8dp"
android:paddingVertical="24dp"
android:textStyle="bold" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/theme_preset_choose_dark"
android:textStyle="bold" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/darkSlotCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:strokeWidth="2dp">
<TextView
android:id="@+id/darkSlotName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="8dp"
android:paddingVertical="24dp"
android:textStyle="bold" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</LinearLayout>

@ -38,7 +38,7 @@
android:layout_marginBottom="12dp"
android:text=""
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurfaceVariant" />
android:textColor="?attr/colorOnSurface" />
<!-- Max Delay Input -->
<com.google.android.material.textfield.TextInputLayout

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/accentCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:strokeWidth="2dp">
<TextView
android:id="@+id/accentName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="12dp"
android:paddingVertical="28dp"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Blue" />
</com.google.android.material.card.MaterialCardView>

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/presetCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:strokeWidth="2dp">
<TextView
android:id="@+id/presetName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="12dp"
android:paddingVertical="28dp"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Ruby" />
</com.google.android.material.card.MaterialCardView>

@ -18,6 +18,7 @@
<style name="Theme.Red" parent="BaseTheme">
<item name="colorPrimary">@color/red_md_theme_dark_primary</item>
<item name="colorPrimaryDark">@color/red_md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/red_md_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/red_md_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/red_md_theme_dark_onPrimaryContainer</item>
@ -49,6 +50,7 @@
<style name="Theme.Blue" parent="BaseTheme">
<item name="colorPrimary">@color/blue_md_theme_dark_primary</item>
<item name="colorPrimaryDark">@color/blue_md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/blue_md_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/blue_md_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/blue_md_theme_dark_onPrimaryContainer</item>
@ -79,6 +81,7 @@
<style name="Theme.Yellow" parent="BaseTheme">
<item name="colorPrimary">@color/yellow_md_theme_dark_primary</item>
<item name="colorPrimaryDark">@color/yellow_md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/yellow_md_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/yellow_md_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/yellow_md_theme_dark_onPrimaryContainer</item>
@ -111,6 +114,7 @@
<style name="Theme.Green" parent="BaseTheme">
<item name="colorPrimary">@color/green_md_theme_dark_primary</item>
<item name="colorPrimaryDark">@color/green_md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/green_md_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/green_md_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/green_md_theme_dark_onPrimaryContainer</item>
@ -143,6 +147,7 @@
<style name="Theme.Purple" parent="BaseTheme">
<item name="colorPrimary">@color/purple_md_theme_dark_primary</item>
<item name="colorPrimaryDark">@color/purple_md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/purple_md_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/purple_md_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/purple_md_theme_dark_onPrimaryContainer</item>
@ -175,6 +180,7 @@
<style name="Theme.Monochrome" parent="BaseTheme">
<item name="colorPrimary">@color/monochrome_theme_dark_primary</item>
<item name="colorPrimaryDark">@color/monochrome_theme_dark_primary</item>
<item name="colorOnPrimary">@color/monochrome_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/monochrome_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/monochrome_theme_dark_onPrimaryContainer</item>
@ -207,6 +213,7 @@
<style name="Theme.Orange" parent="BaseTheme">
<item name="colorPrimary">@color/orange_md_theme_dark_primary</item>
<item name="colorPrimaryDark">@color/orange_md_theme_dark_primary</item>
<item name="colorOnPrimary">@color/orange_md_theme_dark_onPrimary</item>
<item name="colorPrimaryContainer">@color/orange_md_theme_dark_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/orange_md_theme_dark_onPrimaryContainer</item>

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Ported from BAI's 12 named themes (Theme.java / styles.xml / colors.xml).
Two corrections vs. the BAI source:
- "Ukrrooter" is flagged isDark=false in BAI's Theme.java, but its actual
background (#9600ff) and text (#00ff00) are a saturated dark-mode-style
pairing, not a light one — reclassified as dark here.
- "Ruby" had textColorPrimary/textColorSecondary swapped in BAI's source
(a dark grey as the *primary* text color on a dark background) — swapped
back so the higher-contrast color is the main text color.
Each style sets the M3 tokens that give it a distinct identity
(colorPrimary/onPrimary from BAI's accent, colorSurface from BAI's
primary/background, colorOnSurface(Variant) from BAI's text colors);
everything else inherits from BaseTheme. colorPrimaryDark mirrors
colorPrimary (same as BAI's own "Simple" theme template, which maps
both to the same source color) so ThemeUtil.getStyledAppName(), which
reads colorPrimaryDark, shows the preset's own color instead of an
unresolved inherited default.
-->
<string name="theme_preset_classic">Classic</string>
<string name="theme_preset_ruby">Ruby</string>
<string name="theme_preset_rena">Rena</string>
<string name="theme_preset_ukrrooter">Ukrrooter</string>
<string name="theme_preset_amoled">Amoled</string>
<string name="theme_preset_pixel">Pixel</string>
<string name="theme_preset_fdroid_light">F-Droid Light</string>
<string name="theme_preset_dark">Dark</string>
<string name="theme_preset_gold">Gold</string>
<string name="theme_preset_rena_light">Rena Light</string>
<string name="theme_preset_mint">Mint</string>
<string name="theme_preset_fdroid_dark">F-Droid Dark</string>
<string name="use_theme_presets">Use theme presets</string>
<string name="use_theme_presets_summary">A curated set of full color themes, ported from BAI, instead of accent color + light/dark</string>
<string name="theme_preset_auto_mode">Automatic light/dark presets</string>
<string name="theme_preset_auto_mode_summary">Switch between a chosen light preset and a chosen dark preset based on system light/dark mode</string>
<string name="theme_preset_concrete">Theme preset</string>
<string name="theme_preset_light_dark">Light/dark presets</string>
<string name="theme_preset_choose_title">Choose a theme</string>
<string name="theme_preset_choose_light">Light preset</string>
<string name="theme_preset_choose_dark">Dark preset</string>
<style name="ThemePreset.Classic" parent="BaseTheme">
<item name="colorPrimary">#639eff</item>
<item name="colorPrimaryDark">#639eff</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#ffffff</item>
<item name="colorOnSurface">#444444</item>
<item name="colorOnSurfaceVariant">#444444</item>
</style>
<style name="ThemePreset.Ruby" parent="BaseTheme">
<item name="colorPrimary">#ffbcbc</item>
<item name="colorPrimaryDark">#ffbcbc</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#343944</item>
<item name="colorOnSurface">#e5e5e5</item>
<item name="colorOnSurfaceVariant">#c9cacb</item>
</style>
<style name="ThemePreset.Rena" parent="BaseTheme">
<item name="colorPrimary">#e52c5f</item>
<item name="colorPrimaryDark">#e52c5f</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#212735</item>
<item name="colorOnSurface">#f7f4f4</item>
<item name="colorOnSurfaceVariant">#c9cacb</item>
</style>
<style name="ThemePreset.Ukrrooter" parent="BaseTheme">
<item name="colorPrimary">#00ff00</item>
<item name="colorPrimaryDark">#00ff00</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#8a00ee</item>
<item name="colorOnSurface">#00ff00</item>
<item name="colorOnSurfaceVariant">#00ff00</item>
</style>
<style name="ThemePreset.Amoled" parent="BaseTheme">
<item name="colorPrimary">#ffffff</item>
<item name="colorPrimaryDark">#ffffff</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#000000</item>
<item name="colorOnSurface">#ffffff</item>
<item name="colorOnSurfaceVariant">#ffffff</item>
</style>
<style name="ThemePreset.Pixel" parent="BaseTheme">
<item name="colorPrimary">#448aff</item>
<item name="colorPrimaryDark">#448aff</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#ffffff</item>
<item name="colorOnSurface">#de000000</item>
<item name="colorOnSurfaceVariant">#de000000</item>
</style>
<style name="ThemePreset.FDroidLight" parent="BaseTheme">
<item name="colorPrimary">#8bc34a</item>
<item name="colorPrimaryDark">#8bc34a</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#ffffff</item>
<item name="colorOnSurface">#444444</item>
<item name="colorOnSurfaceVariant">#444444</item>
</style>
<style name="ThemePreset.Dark" parent="BaseTheme">
<item name="colorPrimary">#71AAEB</item>
<item name="colorPrimaryDark">#71AAEB</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#131313</item>
<item name="colorOnSurface">#ffffff</item>
<item name="colorOnSurfaceVariant">#e0e0e0</item>
</style>
<style name="ThemePreset.Gold" parent="BaseTheme">
<item name="colorPrimary">#e9b115</item>
<item name="colorPrimaryDark">#e9b115</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#000d26</item>
<item name="colorOnSurface">#e6e6e6</item>
<item name="colorOnSurfaceVariant">#d9d9d9</item>
</style>
<style name="ThemePreset.RenaLight" parent="BaseTheme">
<item name="colorPrimary">#e52c5f</item>
<item name="colorPrimaryDark">#e52c5f</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#ffffff</item>
<item name="colorOnSurface">#444444</item>
<item name="colorOnSurfaceVariant">#444444</item>
</style>
<style name="ThemePreset.Mint" parent="BaseTheme">
<item name="colorPrimary">#86E2B3</item>
<item name="colorPrimaryDark">#86E2B3</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#141F33</item>
<item name="colorOnSurface">#CAD7E0</item>
<item name="colorOnSurfaceVariant">#C3D0D9</item>
</style>
<style name="ThemePreset.FDroidDark" parent="BaseTheme">
<item name="colorPrimary">#8bc34a</item>
<item name="colorPrimaryDark">#8bc34a</item>
<item name="colorOnPrimary">#000000</item>
<item name="colorSurface">#1C1D21</item>
<item name="colorOnSurface">#EDEDED</item>
<item name="colorOnSurfaceVariant">#E6E6E6</item>
</style>
</resources>

@ -16,6 +16,7 @@
<style name="Theme.Red" parent="BaseTheme">
<item name="colorPrimary">@color/red_md_theme_light_primary</item>
<item name="colorPrimaryDark">@color/red_md_theme_light_primary</item>
<item name="colorOnPrimary">@color/red_md_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/red_md_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/red_md_theme_light_onPrimaryContainer</item>
@ -47,6 +48,7 @@
<style name="Theme.Blue" parent="BaseTheme">
<item name="colorPrimary">@color/blue_md_theme_light_primary</item>
<item name="colorPrimaryDark">@color/blue_md_theme_light_primary</item>
<item name="colorOnPrimary">@color/blue_md_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/blue_md_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/blue_md_theme_light_onPrimaryContainer</item>
@ -79,6 +81,7 @@
<style name="Theme.Yellow" parent="BaseTheme">
<item name="colorPrimary">@color/yellow_md_theme_light_primary</item>
<item name="colorPrimaryDark">@color/yellow_md_theme_light_primary</item>
<item name="colorOnPrimary">@color/yellow_md_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/yellow_md_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/yellow_md_theme_light_onPrimaryContainer</item>
@ -112,6 +115,7 @@
<style name="Theme.Green" parent="BaseTheme">
<item name="colorPrimary">@color/green_md_theme_light_primary</item>
<item name="colorPrimaryDark">@color/green_md_theme_light_primary</item>
<item name="colorOnPrimary">@color/green_md_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/green_md_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/green_md_theme_light_onPrimaryContainer</item>
@ -144,6 +148,7 @@
<style name="Theme.Purple" parent="BaseTheme">
<item name="colorPrimary">@color/purple_md_theme_light_primary</item>
<item name="colorPrimaryDark">@color/purple_md_theme_light_primary</item>
<item name="colorOnPrimary">@color/purple_md_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/purple_md_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/purple_md_theme_light_onPrimaryContainer</item>
@ -177,6 +182,7 @@
<style name="Theme.Monochrome" parent="BaseTheme">
<item name="colorPrimary">@color/monochrome_theme_light_primary</item>
<item name="colorPrimaryDark">@color/monochrome_theme_light_primary</item>
<item name="colorOnPrimary">@color/monochrome_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/monochrome_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/monochrome_theme_light_onPrimaryContainer</item>
@ -209,6 +215,7 @@
<style name="Theme.Orange" parent="BaseTheme">
<item name="colorPrimary">@color/orange_md_theme_light_primary</item>
<item name="colorPrimaryDark">@color/orange_md_theme_light_primary</item>
<item name="colorOnPrimary">@color/orange_md_theme_light_onPrimary</item>
<item name="colorPrimaryContainer">@color/orange_md_theme_light_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/orange_md_theme_light_onPrimaryContainer</item>

@ -28,11 +28,8 @@
android:summary="@string/defaultValue"
app:key="ytdlnis_icon" />
<ListPreference
android:entries="@array/accents"
android:entryValues="@array/accents_values"
<Preference
android:icon="@drawable/baseline_color_lens_24"
app:defaultValue="blue"
app:key="theme_accent"
app:summary="Blue"
app:title="@string/accents" />
@ -45,6 +42,28 @@
app:summary="@string/pure_theme_summary"
app:title="@string/high_contrast" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:key="use_theme_presets"
app:summary="@string/use_theme_presets_summary"
app:title="@string/use_theme_presets" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:key="theme_preset_auto_mode"
app:summary="@string/theme_preset_auto_mode_summary"
app:title="@string/theme_preset_auto_mode" />
<Preference
app:key="theme_preset_concrete"
app:title="@string/theme_preset_concrete" />
<Preference
app:key="theme_preset_light_dark"
app:title="@string/theme_preset_light_dark" />
<MultiSelectListPreference
app:icon="@drawable/baseline_hide_image_24"

Loading…
Cancel
Save