diff --git a/.github/workflows/pre_release.yml b/.github/workflows/pre_release.yml index 99ecb7c2..325a9e91 100644 --- a/.github/workflows/pre_release.yml +++ b/.github/workflows/pre_release.yml @@ -1,6 +1,6 @@ name: Pre-Release -# 触发器 +# Trigger on: workflow_dispatch: push: @@ -26,29 +26,53 @@ jobs: uses: actions/checkout@v3 with: repository: ${{ secrets.SECRET_REPO }} - token: ${{ secrets.TOKEN }} # 连接仓库的Token + token: ${{ secrets.TOKEN }} # Repo token path: secret - # 准备 secret 文件 + # Prepare secret files - name: Copy Secret Files run: | cd secret/AniVu cp key.jks ../.. cp secret.gradle.kts ../.. - # 清理 secret 文件 + # Clean secret files - name: Clean Temp Secret Files run: | rm -rf ./secret - # 构建 + # Build - name: Build with Gradle run: | bash ./gradlew assembleGitHubRelease - # 上传 apk - - name: Upload Pre-Release Apk + # Upload apk (arm64-v8a) + - name: Upload Pre-Release Apk (arm64-v8a) uses: actions/upload-artifact@v3 with: - name: Pre-Release Apk - path: app/build/outputs/apk/GitHub/release/*.apk - # 上传 mapping + name: Pre-Release Apk (arm64-v8a) + path: app/build/outputs/apk/GitHub/release/*arm64-v8a*.apk + # Upload apk (armeabi-v7a) + - name: Upload Pre-Release Apk (armeabi-v7a) + uses: actions/upload-artifact@v3 + with: + name: Pre-Release Apk (armeabi-v7a) + path: app/build/outputs/apk/GitHub/release/*armeabi-v7a*.apk + # Upload apk (x86_64) + - name: Upload Pre-Release Apk (x86_64) + uses: actions/upload-artifact@v3 + with: + name: Pre-Release Apk (x86_64) + path: app/build/outputs/apk/GitHub/release/*x86_64*.apk + # Upload apk (x86) + - name: Upload Pre-Release Apk (x86) + uses: actions/upload-artifact@v3 + with: + name: Pre-Release Apk (x86) + path: app/build/outputs/apk/GitHub/release/*x86*.apk + # Upload apk (universal) + - name: Upload Pre-Release Apk (universal) + uses: actions/upload-artifact@v3 + with: + name: Pre-Release Apk (universal) + path: app/build/outputs/apk/GitHub/release/*universal*.apk + # Upload mapping - name: Upload Pre-Release Mapping uses: actions/upload-artifact@v3 with: @@ -57,14 +81,22 @@ jobs: # 获取 apk 路径 - name: Get Pre-Release Apk File Path run: | - echo "PRE_RELEASE_APK=$(find app/build/outputs/apk/GitHub/release -name '*.apk' -type f | head -1)" >> $GITHUB_ENV + echo "PRE_RELEASE_APK_ARM64_V8=$(find app/build/outputs/apk/GitHub/release -name '*arm64-v8a*.apk' -type f | head -1)" >> $GITHUB_ENV + echo "PRE_RELEASE_APK_ARM_V7=$(find app/build/outputs/apk/GitHub/release -name '*armeabi-v7a*.apk' -type f | head -1)" >> $GITHUB_ENV + echo "PRE_RELEASE_APK_X86_64=$(find app/build/outputs/apk/GitHub/release -name '*x86_64*.apk' -type f | head -1)" >> $GITHUB_ENV + echo "PRE_RELEASE_APK_X86=$(find app/build/outputs/apk/GitHub/release -name '*x86*.apk' -type f | head -1)" >> $GITHUB_ENV + echo "PRE_RELEASE_APK_UNIVERSAL=$(find app/build/outputs/apk/GitHub/release -name '*universal*.apk' -type f | head -1)" >> $GITHUB_ENV # 发送至 Telegram 频道 - name: Post to Telegram Channel if: github.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[skip_post]') == false env: CHANNEL_ID: ${{ secrets.TELEGRAM_TO }} BOT_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} - PRE_RELEASE: ${{ env.PRE_RELEASE_APK }} + PRE_RELEASE_ARM64_V8: ${{ env.PRE_RELEASE_APK_ARM64_V8 }} + PRE_RELEASE_ARM_V7: ${{ env.PRE_RELEASE_APK_ARM_V7 }} + PRE_RELEASE_X86_64: ${{ env.PRE_RELEASE_APK_X86_64 }} + PRE_RELEASE_X86: ${{ env.PRE_RELEASE_APK_X86 }} + PRE_RELEASE_UNIVERSAL: ${{ env.PRE_RELEASE_APK_UNIVERSAL }} COMMIT_MESSAGE: |+ GitHub New CI: AniVu\ @@ -75,4 +107,4 @@ jobs: Commit details [here](${{ github.event.head_commit.url }}) run: | ESCAPED=`python3 -c 'import json,os,urllib.parse; print(urllib.parse.quote(json.dumps(os.environ["COMMIT_MESSAGE"])))'` - curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Fpre_release%22%2C%22parse_mode%22%3A%22MarkdownV2%22%2C%22caption%22%3A${ESCAPED}%7D%5D" -F pre_release="@$PRE_RELEASE" + curl -v "https://api.telegram.org/bot${BOT_TOKEN}/sendMediaGroup?chat_id=${CHANNEL_ID}&media=%5B%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Fpre_release_arm64_v8%22%7D%2C%7B%22type%22%3A%22document%22%2C%20%22media%22%3A%22attach%3A%2F%2Fpre_release_arm_v7%22%2C%22parse_mode%22%3A%22MarkdownV2%22%2C%22caption%22%3A${ESCAPED}%7D%5D" -F pre_release_arm64_v8="@$PRE_RELEASE_ARM64_V8" -F pre_release_arm_v7="@$PRE_RELEASE_ARM_V7" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2382236e..7df4d433 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.api.variant.FilterConfiguration import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -20,7 +21,7 @@ android { minSdk = 24 targetSdk = 34 versionCode = 10 - versionName = "1.1-beta02" + versionName = "1.1-beta03" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -49,11 +50,27 @@ android { } } + splits { + abi { + // Enables building multiple APKs per ABI. + isEnable = true + // By default all ABIs are included, so use reset() and include(). + // Resets the list of ABIs for Gradle to create APKs for to none. + reset() + // A list of ABIs for Gradle to create APKs for. + include("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + // We want to also generate a universal APK that includes all ABIs. + isUniversalApk = true + } + } + applicationVariants.all { outputs .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } - .forEach { - it.outputFileName = "AniVu_${versionName}_${buildType.name}_${flavorName}.apk" + .forEach { output -> + val abi = output.getFilter(FilterConfiguration.FilterType.ABI.name) ?: "universal" + output.outputFileName = + "AniVu_${versionName}_${buildType.name}_${abi}_${flavorName}.apk" } } @@ -66,9 +83,6 @@ android { "proguard-rules.pro" ) applicationIdSuffix = ".debug" - ndk { - abiFilters += mutableSetOf("armeabi-v7a", "x86", "x86_64", "arm64-v8a") - } } release { signingConfig = signingConfigs.getByName("release") // signing @@ -78,10 +92,6 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - ndk { - //noinspection ChromeOsAbiSupport - abiFilters += "arm64-v8a" - } } } compileOptions { @@ -155,8 +165,13 @@ dependencies { implementation("io.coil-kt:coil:2.6.0") implementation("com.rometools:rome:2.1.0") implementation("net.dankito.readability4j:readability4j:1.0.8") + implementation("org.libtorrent4j:libtorrent4j-android-arm64:2.1.0-31") + implementation("org.libtorrent4j:libtorrent4j-android-arm:2.1.0-31") + implementation("org.libtorrent4j:libtorrent4j-android-x86:2.1.0-31") + implementation("org.libtorrent4j:libtorrent4j-android-x86_64:2.1.0-31") +// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.13") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/java/com/skyd/anivu/App.kt b/app/src/main/java/com/skyd/anivu/App.kt index 258546d6..7e383101 100644 --- a/app/src/main/java/com/skyd/anivu/App.kt +++ b/app/src/main/java/com/skyd/anivu/App.kt @@ -2,8 +2,10 @@ package com.skyd.anivu import android.app.Application import android.content.Context -import com.google.android.material.color.DynamicColors -import com.skyd.anivu.model.preference.appearance.ThemePreference +import androidx.appcompat.app.AppCompatDelegate +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.getOrDefault +import com.skyd.anivu.model.preference.appearance.DarkModePreference import com.skyd.anivu.model.worker.deletearticle.listenerDeleteArticleFrequency import com.skyd.anivu.model.worker.rsssync.listenerRssSyncFrequency import com.skyd.anivu.util.CrashHandler @@ -16,11 +18,10 @@ class App : Application() { override fun onCreate() { super.onCreate() appContext = this + AppCompatDelegate.setDefaultNightMode(dataStore.getOrDefault(DarkModePreference)) CrashHandler.init(this) -// DynamicColors.applyToActivitiesIfAvailable(this) -// setTheme(ThemePreference.toResId(this)) listenerRssSyncFrequency(this) listenerDeleteArticleFrequency(this) } diff --git a/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt b/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt index d06ad6d2..f48cecd5 100644 --- a/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt @@ -73,4 +73,9 @@ fun Context.getAppName(): String? { e.printStackTrace() null } +} + +fun Context.inDarkMode(): Boolean { + return (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == + Configuration.UI_MODE_NIGHT_YES } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt b/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt index d93c5716..c2363153 100644 --- a/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt @@ -2,7 +2,6 @@ package com.skyd.anivu.ext import android.annotation.TargetApi import android.app.Activity -import android.content.Context import android.graphics.Rect import android.view.DisplayCutout import android.view.View @@ -30,7 +29,6 @@ import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.ExperimentalBadgeUtils import com.skyd.anivu.R -import com.skyd.anivu.appContext fun View.enable() { @@ -74,21 +72,6 @@ val View.activity: Activity val View.tryActivity: Activity? get() = context.tryActivity -fun View.showKeyboard() { - isFocusable = true - isFocusableInTouchMode = true - requestFocus() - val inputManager = - appContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.showSoftInput(this, 0) -} - -fun View.hideKeyboard() { - val inputManager = - appContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputManager.hideSoftInputFromWindow(this.windowToken, 0) -} - /** * 判断View和给定的Rect是否重叠(边和点不计入) * @return true if overlap @@ -238,6 +221,12 @@ fun View.showSoftKeyboard(window: Window) { } } +fun View.hideSoftKeyboard(window: Window) { + if (requestFocus()) { + WindowCompat.getInsetsController(window, this).hide(WindowInsetsCompat.Type.ime()) + } +} + @TargetApi(28) fun View.updateSafeInset(displayCutout: DisplayCutout) { val location = IntArray(2) diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/DarkModePreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/DarkModePreference.kt new file mode 100644 index 00000000..b7663f3f --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/DarkModePreference.kt @@ -0,0 +1,69 @@ +package com.skyd.anivu.model.preference.appearance + +import android.content.Context +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.intPreferencesKey +import com.skyd.anivu.R +import com.skyd.anivu.base.BasePreference +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.put +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +object DarkModePreference : BasePreference { + private const val DARK_MODE = "darkMode" + + val values: List = mutableListOf( + AppCompatDelegate.MODE_NIGHT_NO, + AppCompatDelegate.MODE_NIGHT_YES, + ).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + } + + override val default = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } else { + AppCompatDelegate.MODE_NIGHT_NO + } + + val key = intPreferencesKey(DARK_MODE) + + fun toDisplayName(context: Context, value: Int): String = context.getString( + when (value) { + AppCompatDelegate.MODE_NIGHT_NO -> R.string.dark_mode_light + AppCompatDelegate.MODE_NIGHT_YES -> R.string.dark_mode_dark + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.string.dark_mode_follow_system + else -> R.string.unknown + } + ) + + fun put(context: Context, scope: CoroutineScope, value: Int) { + if (value != AppCompatDelegate.MODE_NIGHT_YES && + value != AppCompatDelegate.MODE_NIGHT_NO && + value != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + ) { + throw IllegalArgumentException("darkMode value invalid!!!") + } + scope.launch(Dispatchers.IO) { + context.dataStore.put(key, value) + withContext(Dispatchers.Main) { + AppCompatDelegate.setDefaultNightMode(value) + } + } + } + + override fun fromPreferences(preferences: Preferences): Int { + val scope = CoroutineScope(context = Dispatchers.Main) + val value = preferences[key] ?: default + scope.launch(Dispatchers.Main) { + AppCompatDelegate.setDefaultNightMode(value) + } + return value + } +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/preference/appearance/ThemePreference.kt b/app/src/main/java/com/skyd/anivu/model/preference/appearance/ThemePreference.kt index a9fc7ea2..508d3b6e 100644 --- a/app/src/main/java/com/skyd/anivu/model/preference/appearance/ThemePreference.kt +++ b/app/src/main/java/com/skyd/anivu/model/preference/appearance/ThemePreference.kt @@ -1,9 +1,9 @@ package com.skyd.anivu.model.preference.appearance import android.content.Context -import android.os.Build import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.stringPreferencesKey +import com.google.android.material.color.DynamicColors import com.skyd.anivu.R import com.skyd.anivu.base.BasePreference import com.skyd.anivu.ext.dataStore @@ -24,13 +24,15 @@ object ThemePreference : BasePreference { const val YELLOW = "Yellow" const val PURPLE = "Purple" + val basicValues = arrayOf(PINK, GREEN, BLUE, YELLOW, PURPLE) + val values: Array get() { - val v = arrayOf(PINK, GREEN, BLUE, YELLOW, PURPLE) - return if (supportDynamicTheme()) arrayOf(DYNAMIC, *v) else v + return if (DynamicColors.isDynamicColorAvailable()) arrayOf(DYNAMIC, *basicValues) + else basicValues } - override val default = if (supportDynamicTheme()) DYNAMIC else PINK + override val default = if (DynamicColors.isDynamicColorAvailable()) DYNAMIC else PINK val key = stringPreferencesKey(THEME) @@ -50,8 +52,6 @@ object ThemePreference : BasePreference { override fun fromPreferences(preferences: Preferences): String = preferences[key] ?: default - private fun supportDynamicTheme(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - fun toDisplayName( context: Context, value: String = context.dataStore.getOrDefault(this), diff --git a/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt b/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt index ba9fc8e4..b2fc0737 100644 --- a/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt +++ b/app/src/main/java/com/skyd/anivu/ui/activity/CrashActivity.kt @@ -57,7 +57,8 @@ class CrashActivity : AppCompatActivity() { append("VersionName: ").append(getAppVersionName()).append("\n") append("Brand: ").append(Build.BRAND).append("\n") append("Model: ").append(Build.MODEL).append("\n") - append("SDK Version: ").append(Build.VERSION.SDK_INT).append("\n\n") + append("SDK Version: ").append(Build.VERSION.SDK_INT).append("\n") + append("ABI: ").append(Build.SUPPORTED_ABIS.firstOrNull().orEmpty()).append("\n\n") append("Crash Info: \n") append(crashInfo) } diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/ColorPalette1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/ColorPalette1Proxy.kt index 8b9508cd..346283fa 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/ColorPalette1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/ColorPalette1Proxy.kt @@ -20,7 +20,7 @@ class ColorPalette1Proxy( ItemColorPalette1Binding .inflate(LayoutInflater.from(parent.context), parent, false), ) - holder.itemView.setOnClickListener { + holder.binding.root.setOnClickListener { onClick(holder.bindingAdapterPosition) } return holder diff --git a/app/src/main/java/com/skyd/anivu/ui/component/preference/ColorPalettesPreference.kt b/app/src/main/java/com/skyd/anivu/ui/component/preference/ColorPalettesPreference.kt index 2c41b15b..1464327c 100644 --- a/app/src/main/java/com/skyd/anivu/ui/component/preference/ColorPalettesPreference.kt +++ b/app/src/main/java/com/skyd/anivu/ui/component/preference/ColorPalettesPreference.kt @@ -1,23 +1,26 @@ package com.skyd.anivu.ui.component.preference import android.content.Context +import android.os.Parcelable import android.util.AttributeSet import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.LayoutManager import com.skyd.anivu.R import com.skyd.anivu.ext.dp import com.skyd.anivu.ui.adapter.variety.VarietyAdapter import com.skyd.anivu.ui.adapter.variety.proxy.ColorPalette1Proxy import com.skyd.anivu.ui.component.SpaceItemDecoration +import kotlinx.parcelize.Parcelize class ColorPalettesPreference : Preference { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : - this(context, attrs, 0) + this(context, attrs, androidx.preference.R.attr.preferenceStyle) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : - this(context, attrs, defStyleAttr, 0) + this(context, attrs, defStyleAttr, R.style.Preference_Material3) constructor( context: Context, @@ -28,10 +31,46 @@ class ColorPalettesPreference : Preference { layoutResource = R.layout.m3_preference_color_palettes } + private var rvLayoutManager: LayoutManager? = null + private var rvLayoutManagerState: Parcelable? = null + + override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + if (isPersistent) { + // No need to save instance state since it's persistent + return superState + } + return SavedState( + superState = superState, + rvLayoutManagerState = rvLayoutManager?.onSaveInstanceState(), + ) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state == null || state.javaClass != SavedState::class.java) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state) + return + } + + val myState = state as SavedState + super.onRestoreInstanceState(myState.superState) + + setRecyclerViewLayoutManagerState(myState.rvLayoutManagerState) + } + + private fun setRecyclerViewLayoutManagerState(state: Parcelable?) { + rvLayoutManagerState = state + notifyChanged() + } + override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) val recyclerView = holder.findViewById(R.id.rv_color_palettes) as? RecyclerView + if (recyclerView?.layoutManager != rvLayoutManager) { + rvLayoutManager = recyclerView?.layoutManager + } if (recyclerView != null) { if (recyclerView.adapter == null) { recyclerView.adapter = adapter @@ -39,14 +78,26 @@ class ColorPalettesPreference : Preference { if (recyclerView.itemDecorationCount == 0) { recyclerView.addItemDecoration(spaceItemDecoration) } + if (rvLayoutManagerState != null) { + recyclerView.layoutManager?.onRestoreInstanceState(rvLayoutManagerState) + rvLayoutManagerState = null + } } } + override fun onDetached() { + super.onDetached() + + rvLayoutManagerState = null + rvLayoutManager = null + } + private val spaceItemDecoration = SpaceItemDecoration(vertical = false, spaceSize = 10.dp) private val adapter = VarietyAdapter(mutableListOf( ColorPalette1Proxy { position -> val colorPalette = colorPalettes[position] val themeName = colorPalette.name + selectedColorPalette = position callChangeListener(themeName) notifyChanged() } @@ -56,6 +107,13 @@ class ColorPalettesPreference : Preference { set(value) { field = value adapter.dataList = value + selectedColorPalette = null + } + + var selectedColorPalette: Int? = null + set(value) { + field = value + notifyChanged() } data class ColorPalette( @@ -66,4 +124,11 @@ class ColorPalettesPreference : Preference { val color3: Int, val description: String, ) + + @Parcelize + data class SavedState( + @JvmField + val superState: Parcelable?, + val rvLayoutManagerState: Parcelable? = null, + ) : BaseSavedState(superState) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/component/preference/ToggleGroupPreference.kt b/app/src/main/java/com/skyd/anivu/ui/component/preference/ToggleGroupPreference.kt new file mode 100644 index 00000000..206bed00 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/ui/component/preference/ToggleGroupPreference.kt @@ -0,0 +1,157 @@ +package com.skyd.anivu.ui.component.preference + +import android.content.Context +import android.os.Parcelable +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import androidx.core.view.setPadding +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.google.android.material.button.MaterialButton +import com.google.android.material.button.MaterialButtonToggleGroup +import com.skyd.anivu.R +import com.skyd.anivu.ext.dp +import kotlinx.parcelize.Parcelize + +class ToggleGroupPreference : Preference { + constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : + this(context, attrs, androidx.preference.R.attr.preferenceStyle) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : + this(context, attrs, defStyleAttr, R.style.Preference_Material3) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) { + layoutResource = R.layout.m3_preference_toggle_group + onPreferenceClickListener = null + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + + holder.itemView.apply { + isClickable = false + isFocusable = false + } + + val toggleGroup = holder.findViewById(R.id.toggleButton) as? MaterialButtonToggleGroup + if (toggleGroup != null) { + toggleGroup.isSingleSelection = isSingleSelection + if (needSetButtons) { + needSetButtons = false + toggleGroup.removeAllViews() + buttons.forEach { (tag, text) -> + toggleGroup.addView( + MaterialButton( + context, + null, + com.google.android.material.R.attr.materialButtonOutlinedStyle, + ).apply { + setPadding(5.dp) + this.tag = tag + this.text = text + }, + LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f) + ) + } + } + needCheckTag.removeIf { tag -> + val id = toggleGroup.findViewWithTag(tag)?.id + if (id != null) toggleGroup.check(id) + true + } + if (addOnButtonCheckedListener) { + addOnButtonCheckedListener = false + toggleGroup.clearOnButtonCheckedListeners() + toggleGroup.addOnButtonCheckedListener { _, id, checked -> + if (checked) { + val button = toggleGroup.findViewById(id) as MaterialButton + checkedButtonIds += id + callChangeListener(button.tag) + } else { + checkedButtonIds -= id + } + } + } + } + } + + private var addOnButtonCheckedListener = true + private var needSetButtons = false + var buttons: List = listOf() + set(value) { + field = value + needSetButtons = true + notifyChanged() + } + + var isSingleSelection: Boolean = true + set(value) { + field = value + notifyChanged() + } + + private val needCheckTag: MutableList = mutableListOf() + fun check(tag: Any) { + needCheckTag += tag + notifyChanged() + } + + fun check(tags: List) { + needCheckTag.addAll(tags) + notifyChanged() + } + + data class ButtonData( + val tag: Any, + val text: String, + ) + + private var checkedButtonIds: MutableList = mutableListOf() + + override fun onDetached() { + super.onDetached() + checkedButtonIds.clear() + } + + override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() + if (isPersistent) { + // No need to save instance state since it's persistent + return superState + } + return SavedState( + superState = superState, + checkedButtonIds = checkedButtonIds, + ) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state == null || state.javaClass != SavedState::class.java) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state) + return + } + + val myState = state as SavedState + super.onRestoreInstanceState(myState.superState) + + myState.checkedButtonIds?.let { checkedButtonIds -> + check(checkedButtonIds) + } + } + + @Parcelize + data class SavedState( + @JvmField + val superState: Parcelable?, + val checkedButtonIds: List? = null, + ) : BaseSavedState(superState) +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt index 05ec2443..89b5245e 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/search/SearchFragment.kt @@ -16,6 +16,7 @@ import com.skyd.anivu.databinding.FragmentSearchBinding import com.skyd.anivu.ext.addInsetsByPadding import com.skyd.anivu.ext.collectIn import com.skyd.anivu.ext.gone +import com.skyd.anivu.ext.hideSoftKeyboard import com.skyd.anivu.ext.popBackStackWithLifecycle import com.skyd.anivu.ext.showSoftKeyboard import com.skyd.anivu.ext.startWith @@ -37,8 +38,14 @@ import java.io.Serializable class SearchFragment : BaseFragment() { @kotlinx.serialization.Serializable sealed interface SearchDomain : Serializable { - data object All : SearchDomain - data object Feed : SearchDomain + data object All : SearchDomain { + private fun readResolve(): Any = All + } + + data object Feed : SearchDomain { + private fun readResolve(): Any = Feed + } + data class Article(val feedUrl: String?) : SearchDomain } @@ -131,6 +138,11 @@ class SearchFragment : BaseFragment() { } } + override fun onStop() { + super.onStop() + binding.tilSearchFragment.editText?.hideSoftKeyboard(window = requireActivity().window) + } + override fun FragmentSearchBinding.setWindowInsets() { tilSearchFragment.addInsetsByPadding(top = true, left = true, right = true) // fabReadFragment.addInsetsByMargin(bottom = true, right = true) diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt index a62a3fd7..67f8659e 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/settings/appearance/AppearanceFragment.kt @@ -6,16 +6,26 @@ import android.util.TypedValue import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceCategory import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import com.google.android.material.color.DynamicColors import com.skyd.anivu.R import com.skyd.anivu.base.BasePreferenceFragmentCompat +import com.skyd.anivu.ext.dataStore +import com.skyd.anivu.ext.getOrDefault +import com.skyd.anivu.ext.inDarkMode +import com.skyd.anivu.model.preference.appearance.DarkModePreference import com.skyd.anivu.model.preference.appearance.ThemePreference import com.skyd.anivu.ui.component.preference.ColorPalettesPreference +import com.skyd.anivu.ui.component.preference.ToggleGroupPreference import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class AppearanceFragment : BasePreferenceFragmentCompat() { override val title by lazy { resources.getString(R.string.appearance_fragment_name) } + + private var useDynamicTheme: SwitchPreferenceCompat? = null + override fun Context.onAddPreferences( savedInstanceState: Bundle?, rootKey: String?, @@ -27,52 +37,104 @@ class AppearanceFragment : BasePreferenceFragmentCompat() { screen.addPreference(this) } - ColorPalettesPreference(this).apply { - key = "colorPalettes" - val iconBackgroundColorTypedValue = TypedValue() - val color1TypedValue = TypedValue() - val color2TypedValue = TypedValue() - val color3TypedValue = TypedValue() - val theme = resources.newTheme() - colorPalettes = (ThemePreference.values.toList() - ThemePreference.DYNAMIC).map { - theme.applyStyle(ThemePreference.toResId(requireContext(), it), true) - - theme.resolveAttribute( - com.google.android.material.R.attr.colorOnPrimaryFixedVariant, - iconBackgroundColorTypedValue, - true - ) - theme.resolveAttribute( - com.google.android.material.R.attr.colorPrimaryFixedDim, - color1TypedValue, - true - ) - theme.resolveAttribute( - com.google.android.material.R.attr.colorSecondaryFixed, - color2TypedValue, - true - ) - theme.resolveAttribute( - com.google.android.material.R.attr.colorTertiaryFixedDim, - color3TypedValue, - true - ) - ColorPalettesPreference.ColorPalette( - name = it, - iconBackgroundColor = iconBackgroundColorTypedValue.data, - color1 = color1TypedValue.data, - color2 = color2TypedValue.data, - color3 = color3TypedValue.data, - description = ThemePreference.toDisplayName(requireContext(), it), + ToggleGroupPreference(this).apply { + buttons = DarkModePreference.values.map { + ToggleGroupPreference.ButtonData( + tag = it, + text = DarkModePreference.toDisplayName(requireContext(), it), ) } + check(requireContext().dataStore.getOrDefault(DarkModePreference)) + setOnPreferenceChangeListener { _, newValue -> + DarkModePreference.put(requireContext(), lifecycleScope, newValue as Int) + true + } + themeCategory.addPreference(this) + } + + val colorPalettesPreference = ColorPalettesPreference(this).apply { + key = "colorPalettes" + isPersistent = false + colorPalettes = getColorPalettes() setOnPreferenceChangeListener { _, newValue -> ThemePreference.put(requireContext(), lifecycleScope, newValue as String) { requireActivity().recreate() + useDynamicTheme?.isChecked = requireContext().dataStore + .getOrDefault(ThemePreference) == ThemePreference.DYNAMIC } true } themeCategory.addPreference(this) } + + if (DynamicColors.isDynamicColorAvailable()) { + useDynamicTheme = SwitchPreferenceCompat(this).apply { + key = "useDynamicTheme" + title = getString(R.string.appearance_fragment_use_dynamic_theme) + summary = getString(R.string.appearance_fragment_use_dynamic_theme_description) + setIcon(R.drawable.ic_colorize_24) + isChecked = + requireContext().dataStore.getOrDefault(ThemePreference) == ThemePreference.DYNAMIC + setOnPreferenceChangeListener { _, newValue -> + ThemePreference.put( + context = requireContext(), + scope = lifecycleScope, + value = if (newValue as Boolean) ThemePreference.DYNAMIC + else ThemePreference.basicValues.first(), + ) { + requireActivity().recreate() + colorPalettesPreference.colorPalettes = getColorPalettes() + } + true + } + themeCategory.addPreference(this) + } + } + } + + private fun getColorPalettes(): List { + val iconBackgroundColorTypedValue = TypedValue() + val color1TypedValue = TypedValue() + val color2TypedValue = TypedValue() + val color3TypedValue = TypedValue() + val theme = resources.newTheme() + return ThemePreference.basicValues.map { + theme.applyStyle(ThemePreference.toResId(requireContext(), it), true) + + theme.resolveAttribute( + if (requireContext().inDarkMode()) { + com.google.android.material.R.attr.colorOnPrimary + } else com.google.android.material.R.attr.colorOnPrimaryFixed, + iconBackgroundColorTypedValue, + true + ) + theme.resolveAttribute( + if (requireContext().inDarkMode()) { + com.google.android.material.R.attr.colorPrimaryContainer + } else com.google.android.material.R.attr.colorPrimary, + color1TypedValue, + true + ) + theme.resolveAttribute( + com.google.android.material.R.attr.colorSecondaryFixedDim, + color2TypedValue, + true + ) + theme.resolveAttribute( + if (requireContext().inDarkMode()) { + com.google.android.material.R.attr.colorTertiaryContainer + } else com.google.android.material.R.attr.colorTertiary, + color3TypedValue, + true + ) + ColorPalettesPreference.ColorPalette( + name = it, + iconBackgroundColor = iconBackgroundColorTypedValue.data, + color1 = color1TypedValue.data, + color2 = color2TypedValue.data, + color3 = color3TypedValue.data, + description = ThemePreference.toDisplayName(requireContext(), it), + ) + } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_colorize_24.xml b/app/src/main/res/drawable/ic_colorize_24.xml new file mode 100644 index 00000000..39a1efb3 --- /dev/null +++ b/app/src/main/res/drawable/ic_colorize_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/m3_preference_color_palettes.xml b/app/src/main/res/layout/m3_preference_color_palettes.xml index 70bc1b1f..4797379c 100644 --- a/app/src/main/res/layout/m3_preference_color_palettes.xml +++ b/app/src/main/res/layout/m3_preference_color_palettes.xml @@ -14,9 +14,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:clipToPadding="false" + android:nestedScrollingEnabled="false" android:orientation="horizontal" + android:paddingHorizontal="26dp" android:paddingVertical="10dp" - android:paddingStart="26dp" - android:paddingEnd="26dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> \ No newline at end of file diff --git a/app/src/main/res/layout/m3_preference_toggle_group.xml b/app/src/main/res/layout/m3_preference_toggle_group.xml new file mode 100644 index 00000000..2d85f1d1 --- /dev/null +++ b/app/src/main/res/layout/m3_preference_toggle_group.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f5077972..fc616d01 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -143,6 +143,11 @@ 外观 +85s 按钮 在播放进度条上方显示 +85s 按钮 + 浅色 + 深色 + 跟随系统 + 动态主题 + 将壁纸颜色应用于主题 每 %d 分钟 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8b83002..5116b3f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,6 +150,11 @@ Appearance +85s button Show +85s button above the progress bar + Light + Dark + System + Dynamic theme + Apply wallpaper colors to the app Every %d minute Every %d minutes