From 53b5b5e475626652d8c73fca8e4cca6d3ed3bb14 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Thu, 30 Apr 2026 13:12:45 +0800 Subject: [PATCH 01/27] feat: skip back to home whitelist --- .../subscreencenter/SubscreenCenterScope.kt | 2 + .../SubScreenBackHomeWhitelistModule.kt | 131 ++++++++++++++++++ .../hk/uwu/reareye/ui/config/ModuleConfig.kt | 14 ++ app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + gradle.properties | 2 +- 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/SubScreenBackHomeWhitelistModule.kt diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt index 1922193..0b08f99 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt @@ -4,6 +4,7 @@ import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker import hk.uwu.reareye.hook.scopes.Scope import hk.uwu.reareye.hook.scopes.subscreencenter.modules.MusicControlWhitelistModule import hk.uwu.reareye.hook.scopes.subscreencenter.modules.RearWallpaperHook +import hk.uwu.reareye.hook.scopes.subscreencenter.modules.SubScreenBackHomeWhitelistModule import hk.uwu.reareye.hook.scopes.subscreencenter.modules.VideoLoopModule import hk.uwu.reareye.hook.scopes.subscreencenter.modules.VideoVolumeHook import hk.uwu.reareye.hook.scopes.subscreencenter.modules.lyrics.LyriconHook @@ -14,6 +15,7 @@ import hk.uwu.reareye.hook.scopes.subscreencenter.modules.rearwidget.SystemUiNot class SubscreenCenterScope : Scope { override val hooks: List = listOf( MusicControlWhitelistModule(), + SubScreenBackHomeWhitelistModule(), VideoLoopModule(), RearWallpaperHook(), RearWidgetHook(), diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/SubScreenBackHomeWhitelistModule.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/SubScreenBackHomeWhitelistModule.kt new file mode 100644 index 0000000..bacef58 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/SubScreenBackHomeWhitelistModule.kt @@ -0,0 +1,131 @@ +package hk.uwu.reareye.hook.scopes.subscreencenter.modules + +import com.highcapable.kavaref.KavaRef.Companion.asResolver +import com.highcapable.kavaref.KavaRef.Companion.resolve +import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog +import hk.uwu.reareye.hook.utils.DexKitMethodInjectionPoint +import hk.uwu.reareye.hook.utils.createDexKitCacheBridge +import hk.uwu.reareye.hook.utils.resolveDexKitFieldValue +import hk.uwu.reareye.hook.utils.resolveDexKitMethodInjectionPoint +import hk.uwu.reareye.hook.utils.resolveHookPackageVersionCode +import hk.uwu.reareye.ui.config.ConfigKeys +import org.luckypray.dexkit.DexKitCacheBridge +import org.luckypray.dexkit.annotations.DexKitExperimentalApi + +@OptIn(DexKitExperimentalApi::class) +class SubScreenBackHomeWhitelistModule : YukiBaseHooker() { + companion object { + private const val SUBSCREEN_HOME_TO_FRONT_METHOD_CACHE_KEY = + "SSC_BACK_HOME_HOME_TO_FRONT_METHOD" + private const val SUBSCREEN_FOREGROUND_PACKAGE_FIELD_CACHE_KEY = + "SSC_BACK_HOME_FOREGROUND_PACKAGE_FIELD" + private const val AOD_REASON = "aod" + } + + override fun onHook() { + loadApp("com.xiaomi.subscreencenter") { + val versionCode = resolveHookPackageVersionCode( + systemContext, + appInfo.packageName, + appInfo.sourceDir, + ) + val bridge = createDexKitCacheBridge( + packageName = appInfo.packageName, + packageVersionCode = versionCode, + sourceDir = appInfo.sourceDir, + dataDir = appInfo.dataDir, + ) + val homeToFrontPoint = resolveSubScreenHomeToFrontMethod(bridge) + val foregroundPackageFieldName = resolveForegroundPackageFieldName( + bridge = bridge, + homeToFrontPoint = homeToFrontPoint, + ) + + homeToFrontPoint.className.toClass().resolve().firstMethod { + name = homeToFrontPoint.methodName + returnType = Void.TYPE + parameters(String::class.java) + }.hook().replaceUnit { + val reason = args(0).cast() + val whitelist = prefs.getStringSet( + ConfigKeys.SUBSCREEN_LOCK_BACK_HOME_WHITELIST_APPS, + ) + if (reason != AOD_REASON || whitelist.isEmpty()) { + invokeOriginal(*args) + return@replaceUnit + } + + val foregroundPackage = instance.asResolver().firstField { + name = foregroundPackageFieldName + type = String::class.java + }.get() + val moreDebug = prefs.getBoolean(ConfigKeys.MORE_DEBUG, false) + if (moreDebug) { + YLog.debug( + "Handle subscreen home return reason=$reason package=$foregroundPackage", + ) + } + if (foregroundPackage == null || foregroundPackage !in whitelist) { + invokeOriginal(*args) + return@replaceUnit + } + + if (moreDebug) { + YLog.debug( + "Skip SubScreen home return reason=$reason package=$foregroundPackage", + ) + } + } + } + } + + private fun resolveSubScreenHomeToFrontMethod( + bridge: DexKitCacheBridge.RecyclableBridge, + ): DexKitMethodInjectionPoint { + return resolveDexKitMethodInjectionPoint( + bridge = bridge, + cacheKey = SUBSCREEN_HOME_TO_FRONT_METHOD_CACHE_KEY, + ) { + // SubScreenCenterApp.e(String) pulls SubScreenLauncher to display 1 for AOD. + findMethod { + searchPackages("com.xiaomi.subscreencenter") + matcher { + paramTypes(String::class.java) + returnType = "void" + usingStrings( + "Start SubScreen Home reason ", + "getHomeToFrontOptions from Aod or turning off", + ) + } + }.singleOrNull() + } ?: error("DexKit failed to resolve SubScreen home-to-front method") + } + + private fun resolveForegroundPackageFieldName( + bridge: DexKitCacheBridge.RecyclableBridge, + homeToFrontPoint: DexKitMethodInjectionPoint, + ): String { + return resolveDexKitFieldValue( + bridge = bridge, + cacheKey = SUBSCREEN_FOREGROUND_PACKAGE_FIELD_CACHE_KEY, + ) { + findField { + searchPackages(homeToFrontPoint.className.substringBeforeLast('.')) + matcher { + declaredClass = homeToFrontPoint.className + type = "java.lang.String" + readMethods { + add { + declaredClass = homeToFrontPoint.className + name = homeToFrontPoint.methodName + paramTypes(String::class.java) + returnType = "void" + usingStrings("Start SubScreen Home reason ") + } + } + } + }.singleOrNull() + } ?: error("DexKit failed to resolve SubScreen foreground package field") + } +} diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt index 7634115..e532a31 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt @@ -26,6 +26,8 @@ object ConfigKeys { const val HOOK_MUSIC_CONTROLS_WHITELIST = "enable_music_controls_whitelist_hook" const val MUSIC_CONTROLS_WHITELIST_APPS = "music_controls_whitelist_apps" + const val SUBSCREEN_LOCK_BACK_HOME_WHITELIST_APPS = + "subscreen_lock_back_home_whitelist_apps" const val HOOK_MUSIC_CONTROLS_FORCE_UPDATE = "enable_music_controls_force_update" const val HOOK_VIDEO_LOOPING = "enable_video_looping" @@ -370,6 +372,18 @@ val REAREyeConfig = listOf( ) ) ), + ConfigCategory( + titleRes = R.string.cfg_subscreen_lock_back_home_whitelist, + descriptionRes = R.string.cfg_subscreen_lock_back_home_whitelist_desc, + children = listOf( + ConfigItem( + key = ConfigKeys.SUBSCREEN_LOCK_BACK_HOME_WHITELIST_APPS, + titleRes = R.string.subscreen_lock_back_home_whitelist_apps, + descriptionRes = R.string.subscreen_lock_back_home_whitelist_apps_desc, + type = ConfigType.AppList(defaultValues = emptySet()) + ) + ) + ), ConfigCategory( titleRes = R.string.subcategory_lyrics, descriptionRes = R.string.subcategory_lyrics_desc, diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 96175dd..9939f90 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -77,6 +77,10 @@ 启用自定义音乐控件白名单 白名单应用 选择哪些应用的音乐控件可以在背屏中显示 + 锁屏返回桌面白名单 + 主屏锁屏进入 AOD 时,阻止背屏中心把选中的应用切回背屏桌面 + 白名单应用 + 选择主屏锁屏后需要继续留在背屏、不要返回背屏桌面的应用 后台白名单 使选中的应用保持在后台运行 启用后台白名单 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0417260..73f5f64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,6 +77,10 @@ Enable Custom Music Controls Whitelist Whitelist Apps Choose which apps music controls can be displayed on the rear screen + Lock Screen Home Return Whitelist + Prevent Subscreen Center from pulling the rear Home to front for selected apps when AOD starts after locking the main screen + Whitelist Apps + Choose apps that should stay on the rear screen instead of returning to Subscreen Home after locking Background Whitelist Keep the selected application running in the background Enable Background Whitelist diff --git a/gradle.properties b/gradle.properties index 32af4e5..c1b82ac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,4 +16,4 @@ project.android.targetSdk=37 project.android.buildToolsVersion=37.0.0 project.android.compileSdkMinor=0 project.app.packageName=hk.uwu.reareye -project.app.versionName="1.0.6" +project.app.versionName="1.0.7" From 90adf0470b3c283bbea5d26d850e3d156cdaa306 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Thu, 30 Apr 2026 18:54:38 +0800 Subject: [PATCH 02/27] feat: disable double tap sleep/wakeup and high load mode for specific apps --- .../reareye/hook/scopes/system/SystemScope.kt | 8 ++- .../DisableSubScreenDoubleTapSleepHook.kt | 64 +++++++++++++++++++ .../DisableSubScreenDoubleTapWakeHook.kt | 47 ++++++++++++++ .../DisableSubScreenHighLoadModeHook.kt | 48 ++++++++++++++ .../hk/uwu/reareye/ui/config/ModuleConfig.kt | 24 +++++++ app/src/main/res/values-zh-rCN/strings.xml | 6 ++ app/src/main/res/values/strings.xml | 6 ++ 7 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapSleepHook.kt create mode 100644 app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt create mode 100644 app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt index 8d31668..a63a405 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt @@ -4,6 +4,9 @@ import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker import hk.uwu.reareye.hook.scopes.Scope import hk.uwu.reareye.hook.scopes.system.modules.BackgroundWhitelistModule import hk.uwu.reareye.hook.scopes.system.modules.DisableRearScreenCoverHook +import hk.uwu.reareye.hook.scopes.system.modules.DisableSubScreenDoubleTapSleepHook +import hk.uwu.reareye.hook.scopes.system.modules.DisableSubScreenDoubleTapWakeHook +import hk.uwu.reareye.hook.scopes.system.modules.DisableSubScreenHighLoadModeHook import hk.uwu.reareye.hook.scopes.system.modules.RearScreenActivityWhitelistModule import hk.uwu.reareye.hook.scopes.system.modules.misc.GMSUnlockModule @@ -13,6 +16,9 @@ class SystemScope : Scope { RearScreenActivityWhitelistModule(), BackgroundWhitelistModule(), GMSUnlockModule(), - DisableRearScreenCoverHook() + DisableRearScreenCoverHook(), + DisableSubScreenDoubleTapSleepHook(), + DisableSubScreenDoubleTapWakeHook(), + DisableSubScreenHighLoadModeHook() ) } \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapSleepHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapSleepHook.kt new file mode 100644 index 0000000..edf6c2a --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapSleepHook.kt @@ -0,0 +1,64 @@ +package hk.uwu.reareye.hook.scopes.system.modules + +import android.view.MotionEvent +import com.highcapable.kavaref.KavaRef.Companion.asResolver +import com.highcapable.kavaref.KavaRef.Companion.resolve +import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog +import hk.uwu.reareye.ui.config.ConfigKeys + +class DisableSubScreenDoubleTapSleepHook : YukiBaseHooker() { + @Volatile + private var focusedPackageName: String? = null + + override fun onHook() { + loadSystem { + val clz = + "com.miui.server.input.gesture.multifingergesture.gesture.MiuiSubscreenDoubleTapGesture" + .toClass() + .resolve() + val managerRef = + "com.miui.server.input.gesture.multifingergesture.MiuiSubScreenMultiFingerGestureManager" + .toClass() + .resolve() + + managerRef.firstMethod { + name = "onFocusedWindowChanged" + parameterCount = 3 + }.hook().after { + focusedPackageName = args(2).any().owningPackage() + } + + clz.firstMethod { + name = "onPointerEvent" + returnType = Void.TYPE + parameters(MotionEvent::class.java) + }.hook().replaceUnit { + val whitelist = prefs.getStringSet( + ConfigKeys.SUBSCREEN_DOUBLE_TAP_SLEEP_DISABLED_APPS, + ) + val packageName = focusedPackageName ?: managerRef.firstMethod { + name = "getFocusedWindow" + }.invoke().owningPackage()?.also { + focusedPackageName = it + } + if (packageName != null && packageName in whitelist) { + if (prefs.getBoolean(ConfigKeys.MORE_DEBUG, false)) { + YLog.debug("Rejected subscreen double tap sleep gesture package=$packageName") + } + return@replaceUnit + } + invokeOriginal(*args) + } + } + } + + private fun Any?.owningPackage(): String? { + return runCatching { + this?.asResolver()?.firstMethod { + name = "getOwningPackage" + returnType = String::class.java + }?.invoke() as? String + }.getOrNull() + } +} diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt new file mode 100644 index 0000000..7b507f6 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt @@ -0,0 +1,47 @@ +package hk.uwu.reareye.hook.scopes.system.modules + +import com.highcapable.kavaref.KavaRef.Companion.resolve +import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog +import hk.uwu.reareye.ui.config.ConfigKeys + +class DisableSubScreenDoubleTapWakeHook : YukiBaseHooker() { + override fun onHook() { + loadSystem { + val dualScreenCoverManagerRef = "com.android.server.power.DualScreenCoverManager" + .toClass() + .resolve() + val windowProcessUtilsRef = "com.android.server.wm.WindowProcessUtils" + .toClass() + .resolve() + + dualScreenCoverManagerRef.firstMethod { + name = "isScreenSkippedWakeup" + parameters(Int::class.java, String::class.java, Int::class.java) + returnType = Boolean::class.java + }.hook().before { + val groupId = args(0).int() + val details = args(1).string() + val packageName = windowProcessUtilsRef.firstMethod { + name = "getTopRunningActivityInfo" + }.invoke().topRunningPackageName() + if (groupId == 1 && details == WAKE_REASON_DOUBLE_TAP && + packageName in prefs.getStringSet( + ConfigKeys.SUBSCREEN_DOUBLE_TAP_WAKE_DISABLED_APPS, + ) + ) { + result = true + if (prefs.getBoolean(ConfigKeys.MORE_DEBUG, false)) { + YLog.debug("Skip subscreen double tap wake package=$packageName") + } + } + } + } + } + + companion object { + private const val WAKE_REASON_DOUBLE_TAP = "android.policy:KEY" + } +} + +private fun Any?.topRunningPackageName() = (this as? Map<*, *>)?.get("packageName") as? String diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt new file mode 100644 index 0000000..7790a70 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt @@ -0,0 +1,48 @@ +package hk.uwu.reareye.hook.scopes.system.modules + +import com.highcapable.kavaref.KavaRef.Companion.resolve +import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog +import hk.uwu.reareye.ui.config.ConfigKeys + +class DisableSubScreenHighLoadModeHook : YukiBaseHooker() { + override fun onHook() { + loadSystem { + val dualScreenCoverManagerRef = "com.android.server.power.DualScreenCoverManager" + .toClass() + .resolve() + val windowProcessUtilsRef = "com.android.server.wm.WindowProcessUtils" + .toClass() + .resolve() + + dualScreenCoverManagerRef.firstMethod { + name = "updateHighLoadSceneMode" + parameters(Int::class.java, Boolean::class.java) + returnType = Void.TYPE + }.hook().replaceUnit { + val value = args(1).boolean() + if (!value) { + invokeOriginal(*args) + return@replaceUnit + } + + val packageName = windowProcessUtilsRef.firstMethod { + name = "getTopRunningActivityInfo" + }.invoke().topRunningPackageName() + if (packageName in prefs.getStringSet( + ConfigKeys.SUBSCREEN_HIGH_LOAD_MODE_DISABLED_APPS, + ) + ) { + result = null + if (prefs.getBoolean(ConfigKeys.MORE_DEBUG, false)) { + YLog.debug("Skip subscreen high load mode package=$packageName") + } + return@replaceUnit + } + invokeOriginal(*args) + } + } + } +} + +private fun Any?.topRunningPackageName() = (this as? Map<*, *>)?.get("packageName") as? String diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt index e532a31..b4c9db3 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt @@ -75,6 +75,12 @@ object ConfigKeys { "enable_take_over_builtin_lyric_handling" const val HOOK_DISABLE_REAR_SCREEN_COVER = "enable_hook_rear_screen_cover" + const val SUBSCREEN_DOUBLE_TAP_SLEEP_DISABLED_APPS = + "subscreen_double_tap_sleep_disabled_apps" + const val SUBSCREEN_DOUBLE_TAP_WAKE_DISABLED_APPS = + "subscreen_double_tap_wake_disabled_apps" + const val SUBSCREEN_HIGH_LOAD_MODE_DISABLED_APPS = + "subscreen_high_load_mode_disabled_apps" const val MORE_DEBUG = "enable_more_debug_logging" const val MODULE_FAVORITE_CONFIG_NODES = "module_favorite_config_nodes" @@ -296,6 +302,24 @@ val REAREyeConfig = listOf( titleRes = R.string.cfg_disable_rear_screen_cover, descriptionRes = R.string.cfg_disable_rear_screen_cover_desc, type = ConfigType.BooleanVal(defaultValue = false) + ), + ConfigItem( + key = ConfigKeys.SUBSCREEN_DOUBLE_TAP_SLEEP_DISABLED_APPS, + titleRes = R.string.cfg_disable_subscreen_double_tap_sleep, + descriptionRes = R.string.cfg_disable_subscreen_double_tap_sleep_desc, + type = ConfigType.AppList(defaultValues = emptySet()) + ), + ConfigItem( + key = ConfigKeys.SUBSCREEN_DOUBLE_TAP_WAKE_DISABLED_APPS, + titleRes = R.string.cfg_disable_subscreen_double_tap_wake, + descriptionRes = R.string.cfg_disable_subscreen_double_tap_wake_desc, + type = ConfigType.AppList(defaultValues = emptySet()) + ), + ConfigItem( + key = ConfigKeys.SUBSCREEN_HIGH_LOAD_MODE_DISABLED_APPS, + titleRes = R.string.cfg_disable_subscreen_high_load_mode, + descriptionRes = R.string.cfg_disable_subscreen_high_load_mode_desc, + type = ConfigType.AppList(defaultValues = emptySet()) ) ) ), diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9939f90..073b2c2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -436,6 +436,12 @@ 设置视频壁纸的播放音量 禁用背屏保护 启用此功能后,当主屏处于全屏模式时,背屏将不会被锁定 + 背屏双击息屏禁用白名单 + 选择需要让系统框架忽略背屏双击息屏/AOD 手势的应用 + 背屏双击唤醒禁用白名单 + 选择在前台时需要忽略背屏双击唤醒的应用 + 背屏高负载模式移除白名单 + 选择在主屏前台时需要跳过背屏高负载模式处理的应用 更多调试输出 歌词 管理歌词配置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 73f5f64..14cd5f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -436,6 +436,12 @@ Widget: %2$s · Priority: %3$d Set playback volume for video wallpapers Disable rear screen cover When enabled, the rear screen will not be locked when the main screen is in full-screen mode + Rear Double-Tap Sleep Disabled Apps + Choose apps where the system framework should ignore rear double-tap sleep/AOD gestures + Rear Double-Tap Wake Disabled Apps + Choose apps where rear double-tap wake should be ignored while they are in the foreground + Rear High Load Mode Disabled Apps + Choose main-screen foreground apps where rear high-load mode handling should be skipped More debug output Lyrics Manage lyrics options From ff84b0f7cca51c2cf6fdf56b6860c64deabe74aa Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Sat, 2 May 2026 10:28:22 +0800 Subject: [PATCH 03/27] fix: resolve deadlock by updating foreground activity method --- .../modules/DisableSubScreenDoubleTapWakeHook.kt | 9 +-------- .../modules/DisableSubScreenHighLoadModeHook.kt | 9 +-------- .../system/modules/SystemForegroundAppResolver.kt | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/SystemForegroundAppResolver.kt diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt index 7b507f6..f3bfc34 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenDoubleTapWakeHook.kt @@ -11,9 +11,6 @@ class DisableSubScreenDoubleTapWakeHook : YukiBaseHooker() { val dualScreenCoverManagerRef = "com.android.server.power.DualScreenCoverManager" .toClass() .resolve() - val windowProcessUtilsRef = "com.android.server.wm.WindowProcessUtils" - .toClass() - .resolve() dualScreenCoverManagerRef.firstMethod { name = "isScreenSkippedWakeup" @@ -22,9 +19,7 @@ class DisableSubScreenDoubleTapWakeHook : YukiBaseHooker() { }.hook().before { val groupId = args(0).int() val details = args(1).string() - val packageName = windowProcessUtilsRef.firstMethod { - name = "getTopRunningActivityInfo" - }.invoke().topRunningPackageName() + val packageName = instance.mainDisplayForegroundPackageName() if (groupId == 1 && details == WAKE_REASON_DOUBLE_TAP && packageName in prefs.getStringSet( ConfigKeys.SUBSCREEN_DOUBLE_TAP_WAKE_DISABLED_APPS, @@ -43,5 +38,3 @@ class DisableSubScreenDoubleTapWakeHook : YukiBaseHooker() { private const val WAKE_REASON_DOUBLE_TAP = "android.policy:KEY" } } - -private fun Any?.topRunningPackageName() = (this as? Map<*, *>)?.get("packageName") as? String diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt index 7790a70..60d23fa 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/DisableSubScreenHighLoadModeHook.kt @@ -11,9 +11,6 @@ class DisableSubScreenHighLoadModeHook : YukiBaseHooker() { val dualScreenCoverManagerRef = "com.android.server.power.DualScreenCoverManager" .toClass() .resolve() - val windowProcessUtilsRef = "com.android.server.wm.WindowProcessUtils" - .toClass() - .resolve() dualScreenCoverManagerRef.firstMethod { name = "updateHighLoadSceneMode" @@ -26,9 +23,7 @@ class DisableSubScreenHighLoadModeHook : YukiBaseHooker() { return@replaceUnit } - val packageName = windowProcessUtilsRef.firstMethod { - name = "getTopRunningActivityInfo" - }.invoke().topRunningPackageName() + val packageName = instance.mainDisplayForegroundPackageName() if (packageName in prefs.getStringSet( ConfigKeys.SUBSCREEN_HIGH_LOAD_MODE_DISABLED_APPS, ) @@ -44,5 +39,3 @@ class DisableSubScreenHighLoadModeHook : YukiBaseHooker() { } } } - -private fun Any?.topRunningPackageName() = (this as? Map<*, *>)?.get("packageName") as? String diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/SystemForegroundAppResolver.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/SystemForegroundAppResolver.kt new file mode 100644 index 0000000..8f65b4a --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/SystemForegroundAppResolver.kt @@ -0,0 +1,15 @@ +package hk.uwu.reareye.hook.scopes.system.modules + +import com.highcapable.kavaref.KavaRef.Companion.asResolver + +internal fun Any?.mainDisplayForegroundPackageName(): String? = runCatching { + val instance = this ?: return null + val powerManagerServiceImpl = instance.asResolver().firstField { + name = "mPowerManagerServiceImpl" + }.get() ?: return null + + powerManagerServiceImpl.asResolver().firstField { + name = "mForegroundAppPackageName" + type = String::class.java + }.get() +}.getOrNull() From a71932137bd938c4503ec78283fca50d0dfc635d Mon Sep 17 00:00:00 2001 From: ghhccghk <2137610394@qq.com> Date: Fri, 1 May 2026 21:30:14 +0800 Subject: [PATCH 04/27] ui: replace CircularProgressIndicator with InfiniteProgressIndicator in RearStoreScreen and AboutScreen Signed-off-by: ghhccghk <2137610394@qq.com> --- app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt | 4 ++-- app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 178574d..34ad209 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -99,9 +99,9 @@ import okhttp3.Request import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CardDefaults -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.ScrollBehavior @@ -777,7 +777,7 @@ private fun ContributorListContent( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Text(text = stringResource(R.string.credits_contributors_loading)) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt index d44a70a..d76394a 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt @@ -143,6 +143,7 @@ import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.HorizontalDivider import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior @@ -2722,7 +2723,7 @@ private fun RearStoreLoadingCard() { horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Text( text = stringResource(R.string.rear_widget_loading_data), modifier = Modifier.padding(start = 10.dp), From 14426262b23b78e2bb371e7f158acd6a3cf9fe32 Mon Sep 17 00:00:00 2001 From: ghhccghk <2137610394@qq.com> Date: Fri, 1 May 2026 22:26:28 +0800 Subject: [PATCH 05/27] feat: add device and OS information display in About screen - Added `PropTools` and `OSVersionTools` to retrieve system properties including device name, market name, Android version, and OS version (with support for root-access naming). - Updated `AboutScreen` to display a new card containing device and system specifications. - Added necessary string resources for device info labels in both English and Chinese. - Refactored `InfoLine` component in `HomeScreen` for better layout flexibility. Signed-off-by: ghhccghk <2137610394@qq.com> --- .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 54 +++++++++- .../hk/uwu/reareye/ui/screen/HomeScreen.kt | 18 ++-- .../uwu/reareye/utils/other/OSVersionTools.kt | 99 +++++++++++++++++++ .../hk/uwu/reareye/utils/other/PropTools.kt | 44 +++++++++ app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 6 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt create mode 100644 app/src/main/java/hk/uwu/reareye/utils/other/PropTools.kt diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 34ad209..812dcc1 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -91,11 +91,14 @@ import hk.uwu.reareye.ui.theme.rememberAcrylicHazeState import hk.uwu.reareye.ui.theme.rememberAcrylicHazeStyle import hk.uwu.reareye.utils.blend.ColorBlendToken import hk.uwu.reareye.utils.effect.BgEffectBackground +import hk.uwu.reareye.utils.other.OSVersionTools +import hk.uwu.reareye.utils.other.PropTools import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CardDefaults @@ -638,9 +641,58 @@ private fun AboutRootContent( item(key = "content") { Column( modifier = Modifier - .fillParentMaxHeight() + .fillMaxWidth() .padding(bottom = scrollPadding.calculateBottomPadding()), ) { + ArtStaggeredReveal( + visible = true, + revealKey = "contributors", + delayMillis = 36, + ) { + Card( + modifier = Modifier + .padding(horizontal = 8.dp) + .padding(bottom = 8.dp) + .textureBlur( + backdrop = backdrop, + shape = SmoothRoundedCornerShape(16.dp), + blurRadius = 60f, + noiseCoefficient = 0.001f, + colors = BlurColors( + blendColors = visualTokens.cardBlendColors, + brightness = 0f, + contrast = 1f, + saturation = 1f, + ), + enabled = true, + ), + colors = CardDefaults.defaultColors( + Color.Transparent, + Color.Transparent, + ), + ) { + BasicComponent( + title = PropTools.deviceName, + ) + + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.device_name), + summary = PropTools.marketName, + ) + + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.android_version), + summary = PropTools.androidVersion, + ) + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.os_version), + summary = OSVersionTools.addVersionSuffix(context), + ) + + + } + } + ArtStaggeredReveal( visible = true, revealKey = "contributors", diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt index 17498d2..811723e 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt @@ -889,12 +889,14 @@ private fun UpdateInfoCard(currentHash: String, latestHash: String?, checking: B } @Composable -private fun InfoLine(title: String, value: String) { - Text(text = title, style = MiuixTheme.textStyles.headline1) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = value, - style = MiuixTheme.textStyles.body2, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, - ) +fun InfoLine(title: String, value: String,modifier: Modifier = Modifier) { + Column(modifier = modifier) { + Text(text = title, style = MiuixTheme.textStyles.headline1) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = value, + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + } } diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt new file mode 100644 index 0000000..43eb700 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt @@ -0,0 +1,99 @@ +package hk.uwu.reareye.utils.other + +import android.content.Context +import android.util.Log +import hk.uwu.reareye.R +import hk.uwu.reareye.utils.other.PropTools.getSystemProperties +import java.util.regex.Pattern + +object OSVersionTools { + + private const val TAG = "OSVersionTools" + private val VERSION_PATTERN: Pattern = Pattern.compile("^[a-zA-Z][0-9]{1,3}$") + + fun getXmsVersion(): String { + return getSystemProperties("persist.sys.xms.version") + } + + fun getRoXmsVersion(): String { + return getSystemProperties("ro.mi.xms.version.incremental") + } + + fun getOsVersionCode(): String { + val str = getSystemProperties("ro.mi.os.version.incremental") + return if (str.isEmpty() || !str.startsWith("OS") || str.length <= 2) { + str + } else { + str.substring(2) + } + } + + fun addVersionSuffix(context: Context?): String { + var xmsVersion = getXmsVersion() + val roXmsVersion = getRoXmsVersion() + val osVersionCode = getOsVersionCode() + + val isXmsValid = isValid(xmsVersion) + val isRoValid = isValid(roXmsVersion) + + if (!isXmsValid && !isRoValid) { + return osVersionCode + } + + if (!isXmsValid || isRoValid) { + xmsVersion = if (isXmsValid || !isRoValid) { + compareValidVersion(xmsVersion, roXmsVersion) + } else { + roXmsVersion + } + } + + return insertSuffixBeforeBeta(context, osVersionCode, xmsVersion) + } + + private fun isValid(str: String?): Boolean { + return !str.isNullOrEmpty() && VERSION_PATTERN.matcher(str).matches() + } + + private fun compareValidVersion(s: String, s2: String): String { + val c1 = s[0].lowercaseChar() + val c2 = s2[0].lowercaseChar() + + if (c1 != c2) { + return if (c1 > c2) s else s2 + } + + return try { + val v1 = s.substring(1).toInt() + val v2 = s2.substring(1).toInt() + if (v1 >= v2) s else s2 + } catch (e: Exception) { + Log.d(TAG, "compareValidVersion: parse failed $e") + s + } + } + + private fun insertSuffixBeforeBeta( + context: Context?, + base: String, + suffix: String + ): String { + var tail: String? = null + + val devString = context?.getString(R.string.developer_build) + + if (devString != null && base.endsWith(devString)) { + tail = devString + } + + if (base.endsWith("Beta")) { + tail = "Beta" + } + + return if (tail != null) { + base.removeSuffix(tail).trim() + ".$suffix $tail" + } else { + "$base.$suffix" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/PropTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/PropTools.kt new file mode 100644 index 0000000..45c1012 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/utils/other/PropTools.kt @@ -0,0 +1,44 @@ +package hk.uwu.reareye.utils.other + +import android.annotation.SuppressLint +import android.os.Build +import hk.uwu.reareye.utils.RootHelper.executeRootCommand +import hk.uwu.reareye.utils.RootHelper.hasRootAccess + +object PropTools { + + // 我就说我设备名字怎么就对不上了,这玩意还要 Root 获取,破烂 + val getdevice = if (hasRootAccess()) executeRootCommand("getprop persist.private.device_name") else Pair(20, "无法获取Root来获取设备名字") + val deviceName = if (getdevice.first == 0) getdevice.second else "无法获取Root来获取设备名字" + + + val androidVersion: String = getSystemProperties("ro.build.version.release") + + val marketName by lazy { + + val marketName: String = getSystemProperties("ro.product.marketname") + + if (marketName.isNotEmpty()) bigtextone(marketName) else bigtextone(Build.BRAND) + " " + Build.MODEL + + } + + fun bigtextone(st:String): String { + val formattedBrand = st.replaceFirstChar { + if (it.isLowerCase()) it.titlecase() else it.toString() + } + return formattedBrand + } + + @SuppressLint("PrivateApi") + + fun getSystemProperties(key: String): String { + val ret: String = try { + Class.forName("android.os.SystemProperties").getDeclaredMethod("get", String::class.java).invoke(null, key) as String + } catch (iAE: IllegalArgumentException) { + throw iAE + } catch (e: Exception) { + "" + } + return ret + } +} \ 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 073b2c2..2edf0b1 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -470,4 +470,8 @@ 临无人所及之境 呈巅峰造极形态 非凡大师 + 开发版 + 设备型号 + OS 版本 + Android 版本 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14cd5f0..37f3b9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -470,4 +470,8 @@ Widget: %2$s · Priority: %3$d Explore the ultimate frontier Unfold in front of you ULTIMATE DESIGN + DEV + Device Name + OS Version + Android Version From 42dd4c64ef59281b9bda800b61924db95419bd72 Mon Sep 17 00:00:00 2001 From: ghhccghk <2137610394@qq.com> Date: Sat, 2 May 2026 16:14:39 +0800 Subject: [PATCH 06/27] refactor: replace PropTools with DeviceConfigTools for device information and update loading indicators to InfiniteProgressIndicator Signed-off-by: ghhccghk <2137610394@qq.com> --- .../reareye/ui/components/config/AppListSelectorScreen.kt | 4 ++-- .../ui/components/config/BusinessExtraConfigScreen.kt | 4 ++-- .../reareye/ui/components/config/BusinessManagerScreen.kt | 4 ++-- .../uwu/reareye/ui/components/config/CardManagerScreen.kt | 4 ++-- .../components/config/RearWallpaperManagementContent.kt | 4 ++-- .../ui/components/config/RearWallpaperManagerScreen.kt | 6 +++--- .../ui/components/config/SceneRouteManagerScreen.kt | 4 ++-- .../config/template/TemplateVarConfigScreenScaffold.kt | 4 ++-- app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt | 8 ++++---- .../main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt | 3 +-- .../utils/other/{PropTools.kt => DeviceConfigTools.kt} | 4 ++-- .../java/hk/uwu/reareye/utils/other/OSVersionTools.kt | 2 +- 12 files changed, 25 insertions(+), 26 deletions(-) rename app/src/main/java/hk/uwu/reareye/utils/other/{PropTools.kt => DeviceConfigTools.kt} (97%) diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/AppListSelectorScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/AppListSelectorScreen.kt index 28c8d89..661e4b4 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/AppListSelectorScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/AppListSelectorScreen.kt @@ -73,11 +73,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.FabPosition import top.yukonga.miuix.kmp.basic.FloatingActionButton import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior @@ -221,7 +221,7 @@ private fun LoadingAppsCard() { modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() } } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessExtraConfigScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessExtraConfigScreen.kt index 626c97f..cbcfc09 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessExtraConfigScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessExtraConfigScreen.kt @@ -63,9 +63,9 @@ import kotlinx.coroutines.withContext import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Text @@ -293,7 +293,7 @@ fun BusinessExtraConfigManagerScreen( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Text(text = stringResource(R.string.rear_widget_loading_data)) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessManagerScreen.kt index 90e53b4..2ff55f0 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/BusinessManagerScreen.kt @@ -65,9 +65,9 @@ import kotlinx.coroutines.withContext import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Text @@ -411,7 +411,7 @@ fun BusinessManagerScreen( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Text(text = stringResource(R.string.rear_widget_loading_data)) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt index 0af9477..79960df 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt @@ -72,9 +72,9 @@ import kotlinx.coroutines.withContext import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Switch @@ -433,7 +433,7 @@ fun CardManagerScreen( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Text(text = stringResource(R.string.rear_widget_loading_data)) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagementContent.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagementContent.kt index e8bf09f..687cef6 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagementContent.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagementContent.kt @@ -62,8 +62,8 @@ import top.yukonga.miuix.kmp.basic.BasicComponentDefaults import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.Switch import top.yukonga.miuix.kmp.basic.Text @@ -492,7 +492,7 @@ private fun RearWallpaperManagementList( Card(modifier = Modifier.fillMaxWidth()) { SuperCard( title = stringResource(R.string.rear_wallpaper_loading), - startAction = { CircularProgressIndicator() }, + startAction = { InfiniteProgressIndicator() }, ) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt index ca53be0..f70f96e 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt @@ -112,9 +112,9 @@ import top.yukonga.miuix.kmp.basic.BasicComponentDefaults import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Switch @@ -862,7 +862,7 @@ fun RearWallpaperManagerScreen( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Spacer(Modifier.width(10.dp)) Text(text = stringResource(R.string.rear_wallpaper_loading)) } @@ -969,7 +969,7 @@ fun RearWallpaperManagerScreen( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Spacer(Modifier.width(10.dp)) Text(text = stringResource(R.string.rear_wallpaper_loading)) } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/SceneRouteManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/SceneRouteManagerScreen.kt index 0d8893d..11a6648 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/SceneRouteManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/SceneRouteManagerScreen.kt @@ -58,9 +58,9 @@ import kotlinx.coroutines.withContext import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Text @@ -279,7 +279,7 @@ fun SceneRouteManagerScreen( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Text(text = stringResource(R.string.rear_widget_loading_data)) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/template/TemplateVarConfigScreenScaffold.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/template/TemplateVarConfigScreenScaffold.kt index af8cc59..79aba59 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/template/TemplateVarConfigScreenScaffold.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/template/TemplateVarConfigScreenScaffold.kt @@ -27,9 +27,9 @@ import hk.uwu.reareye.ui.theme.rememberAcrylicHazeState import hk.uwu.reareye.ui.theme.rememberAcrylicHazeStyle import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Text @@ -92,7 +92,7 @@ fun TemplateVarConfigScreenScaffold( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() Text(text = loadingText) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 812dcc1..603202d 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -91,8 +91,8 @@ import hk.uwu.reareye.ui.theme.rememberAcrylicHazeState import hk.uwu.reareye.ui.theme.rememberAcrylicHazeStyle import hk.uwu.reareye.utils.blend.ColorBlendToken import hk.uwu.reareye.utils.effect.BgEffectBackground +import hk.uwu.reareye.utils.other.DeviceConfigTools import hk.uwu.reareye.utils.other.OSVersionTools -import hk.uwu.reareye.utils.other.PropTools import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext @@ -672,17 +672,17 @@ private fun AboutRootContent( ), ) { BasicComponent( - title = PropTools.deviceName, + title = DeviceConfigTools.deviceName, ) BasicComponent( title = androidx.compose.ui.res.stringResource(R.string.device_name), - summary = PropTools.marketName, + summary = DeviceConfigTools.marketName, ) BasicComponent( title = androidx.compose.ui.res.stringResource(R.string.android_version), - summary = PropTools.androidVersion, + summary = DeviceConfigTools.androidVersion, ) BasicComponent( title = androidx.compose.ui.res.stringResource(R.string.os_version), diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt index d76394a..61f09d5 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt @@ -139,7 +139,6 @@ import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CardDefaults -import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.HorizontalDivider import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton @@ -1311,7 +1310,7 @@ private fun RearStoreDetailContent( .padding(paddingValues), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator() + InfiniteProgressIndicator() } return@Scaffold } diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/PropTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt similarity index 97% rename from app/src/main/java/hk/uwu/reareye/utils/other/PropTools.kt rename to app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt index 45c1012..43970a3 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/PropTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt @@ -5,7 +5,7 @@ import android.os.Build import hk.uwu.reareye.utils.RootHelper.executeRootCommand import hk.uwu.reareye.utils.RootHelper.hasRootAccess -object PropTools { +object DeviceConfigTools { // 我就说我设备名字怎么就对不上了,这玩意还要 Root 获取,破烂 val getdevice = if (hasRootAccess()) executeRootCommand("getprop persist.private.device_name") else Pair(20, "无法获取Root来获取设备名字") @@ -13,7 +13,7 @@ object PropTools { val androidVersion: String = getSystemProperties("ro.build.version.release") - + val marketName by lazy { val marketName: String = getSystemProperties("ro.product.marketname") diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt index 43eb700..acabd50 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt @@ -3,7 +3,7 @@ package hk.uwu.reareye.utils.other import android.content.Context import android.util.Log import hk.uwu.reareye.R -import hk.uwu.reareye.utils.other.PropTools.getSystemProperties +import hk.uwu.reareye.utils.other.DeviceConfigTools.getSystemProperties import java.util.regex.Pattern object OSVersionTools { From 75c66af1ffcaf97d5049e5523ca01c6f2a966739 Mon Sep 17 00:00:00 2001 From: ghhccghk <2137610394@qq.com> Date: Sat, 2 May 2026 16:48:20 +0800 Subject: [PATCH 07/27] feat: add sub-screen version display and optimize blur effects - **feat**: Added ability to retrieve and display "Sub-screen Version" (targeting `com.xiaomi.subscreencenter`) in the About screen. - **feat**: Implemented `rememberBlurBackdrop` utility and integrated `BlurredBar` into the navigation bar for better visual effects. - **fix**: Fixed package name inconsistency for `PageUtils` and updated related imports. - **ui**: Refined `BlurredBar` blur radius calculation using density and adjusted surface color opacity. - **refactor**: Cleaned up `AboutScreen` UI code and improved device name display styling. - **manifest**: Added `` for the sub-screen center package to comply with Android 11+ package visibility requirements. Signed-off-by: ghhccghk <2137610394@qq.com> --- app/src/main/AndroidManifest.xml | 5 +++ .../navigation/RearNavigationBar.kt | 34 +++++++++++-------- .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 19 +++++++++-- .../java/hk/uwu/reareye/utils/PageUtils.kt | 12 +++---- .../hk/uwu/reareye/utils/blend/BlurExt.kt | 17 ++++++++++ .../reareye/utils/other/DeviceConfigTools.kt | 7 ++++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 71 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/hk/uwu/reareye/utils/blend/BlurExt.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cdd9e1d..19fd968 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -88,5 +88,10 @@ android:value="false" /> + + + + + diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt index adf4de7..7b7a9d3 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt @@ -1,6 +1,5 @@ package hk.uwu.reareye.ui.components.navigation -import android.os.Build import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.WindowInsets @@ -17,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -25,6 +25,8 @@ import androidx.compose.ui.unit.sp import com.kyant.backdrop.Backdrop import hk.uwu.reareye.R import hk.uwu.reareye.ui.config.ModuleNavigationBarMode +import hk.uwu.reareye.utils.BlurredBar +import hk.uwu.reareye.utils.blend.rememberBlurBackdrop import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.NavigationBar import top.yukonga.miuix.kmp.basic.NavigationBarDisplayMode @@ -83,25 +85,27 @@ fun RearNavigationBar( } if (navigationBarMode == ModuleNavigationBarMode.NORMAL) { - NavigationBar( - modifier = modifier, - color = MiuixTheme.colorScheme.surface, - mode = NavigationBarDisplayMode.IconAndText, - ) { - items.forEach { item -> - NavigationBarItem( - selected = currentScreen == item.route, - onClick = { onScreenSelected(item.route) }, - icon = item.icon, - label = item.label, - ) + val enable = rememberBlurBackdrop(true) + BlurredBar(backdrop = enable){ + NavigationBar( + modifier = modifier, + color = Color.Transparent, + mode = NavigationBarDisplayMode.IconAndText, + ) { + items.forEach { item -> + NavigationBarItem( + selected = currentScreen == item.route, + onClick = { onScreenSelected(item.route) }, + icon = item.icon, + label = item.label, + ) + } } } return } - val enableGlass = navigationBarMode == ModuleNavigationBarMode.FLOATING_GLASS && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + val enableGlass = navigationBarMode == ModuleNavigationBarMode.FLOATING_GLASS val selectedIndex = items.indexOfFirst { it.route == currentScreen }.coerceAtLeast(0) FloatingBottomBar( diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 603202d..b37c76d 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -93,12 +93,14 @@ import hk.uwu.reareye.utils.blend.ColorBlendToken import hk.uwu.reareye.utils.effect.BgEffectBackground import hk.uwu.reareye.utils.other.DeviceConfigTools import hk.uwu.reareye.utils.other.OSVersionTools +import hk.uwu.reareye.utils.pageContentPadding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import top.yukonga.miuix.kmp.basic.BasicComponent +import top.yukonga.miuix.kmp.basic.BasicComponentDefaults import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CardDefaults @@ -125,7 +127,6 @@ import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic -import utils.pageContentPadding import java.util.concurrent.ConcurrentHashMap private val contributorAvatarHttpClient = OkHttpClient() @@ -672,8 +673,15 @@ private fun AboutRootContent( ), ) { BasicComponent( - title = DeviceConfigTools.deviceName, - ) + enabled = true, + ) { + Text( + text = DeviceConfigTools.deviceName, + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + color = BasicComponentDefaults.titleColor().color, + ) + } BasicComponent( title = androidx.compose.ui.res.stringResource(R.string.device_name), @@ -689,6 +697,11 @@ private fun AboutRootContent( summary = OSVersionTools.addVersionSuffix(context), ) + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.subsceen_version), + summary = DeviceConfigTools.getSubSceenVersion(context), + ) + } } diff --git a/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt b/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt index e2322dd..a310a29 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt @@ -1,7 +1,7 @@ // Copyright 2025, compose-miuix-ui contributors // SPDX-License-Identifier: Apache-2.0 -package utils +package hk.uwu.reareye.utils import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import top.yukonga.miuix.kmp.basic.ScrollBehavior @@ -24,8 +25,6 @@ import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.blur.BlendColorEntry import top.yukonga.miuix.kmp.blur.BlurColors import top.yukonga.miuix.kmp.blur.LayerBackdrop -import top.yukonga.miuix.kmp.blur.isRenderEffectSupported -import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop import top.yukonga.miuix.kmp.blur.textureBlur import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.utils.overScrollVertical @@ -107,18 +106,17 @@ fun AdaptiveTopAppBar( @Composable fun BlurredBar( backdrop: LayerBackdrop?, - blurEnabled: Boolean, content: @Composable () -> Unit, ) { Box( - modifier = if (blurEnabled && backdrop != null) { + modifier = if (backdrop != null) { Modifier.textureBlur( backdrop = backdrop, shape = RectangleShape, - blurRadius = 25f, + blurRadius = 25f * LocalDensity.current.density, colors = BlurColors( blendColors = listOf( - BlendColorEntry(color = MiuixTheme.colorScheme.surface.copy(0.8f)), + BlendColorEntry(color = MiuixTheme.colorScheme.surface.copy(0.87f)), ), ), ) diff --git a/app/src/main/java/hk/uwu/reareye/utils/blend/BlurExt.kt b/app/src/main/java/hk/uwu/reareye/utils/blend/BlurExt.kt new file mode 100644 index 0000000..7df3922 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/utils/blend/BlurExt.kt @@ -0,0 +1,17 @@ +package hk.uwu.reareye.utils.blend + +import androidx.compose.runtime.Composable +import top.yukonga.miuix.kmp.blur.LayerBackdrop +import top.yukonga.miuix.kmp.blur.isRenderEffectSupported +import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Composable +fun rememberBlurBackdrop(enableBlur: Boolean): LayerBackdrop? { + if (!enableBlur || !isRenderEffectSupported()) return null + val surfaceColor = MiuixTheme.colorScheme.surface + return rememberLayerBackdrop { + drawRect(surfaceColor) + drawContent() + } +} diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt index 43970a3..c65682e 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt @@ -1,6 +1,7 @@ package hk.uwu.reareye.utils.other import android.annotation.SuppressLint +import android.content.Context import android.os.Build import hk.uwu.reareye.utils.RootHelper.executeRootCommand import hk.uwu.reareye.utils.RootHelper.hasRootAccess @@ -41,4 +42,10 @@ object DeviceConfigTools { } return ret } + + fun getSubSceenVersion(context: Context): String { + val packageManager = context.packageManager + val packageInfo = packageManager.getPackageInfo("com.xiaomi.subscreencenter", 0) + return packageInfo.versionName.toString() + } } \ 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 2edf0b1..1638f75 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -474,4 +474,5 @@ 设备型号 OS 版本 Android 版本 + 背屏版本 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37f3b9f..3fd4ebf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -474,4 +474,5 @@ Widget: %2$s · Priority: %3$d Device Name OS Version Android Version + SubSceen Version From 106d512749e04eb85ed9ffbcb612b8c88d9d52c8 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 4 May 2026 21:16:33 +0800 Subject: [PATCH 08/27] chore: improve about screen layout & the long-press trigger logic for favorites has been modified to fix the issue of overly sensitive triggering & formatting code --- .../modules/RearWallpaperHook.kt | 128 +-- .../modules/rearwidget/RearWidgetHook.kt | 16 +- .../modules/UnlockVideoRestrictionsHook.kt | 904 ++++++++-------- .../ui/components/config/CardManagerScreen.kt | 561 +++++----- .../config/RearWallpaperManagerScreen.kt | 963 +++++++++--------- .../navigation/RearNavigationBar.kt | 13 +- .../hk/uwu/reareye/ui/config/ModuleConfig.kt | 6 + .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 216 ++-- .../hk/uwu/reareye/ui/screen/ConfigScreen.kt | 21 +- .../hk/uwu/reareye/ui/screen/HomeScreen.kt | 2 +- .../java/hk/uwu/reareye/utils/PageUtils.kt | 63 +- .../reareye/utils/blend/BlendTokenConfig.kt | 95 -- .../reareye/utils/effect/FrameTimeSeconds.kt | 2 +- .../reareye/utils/other/DeviceConfigTools.kt | 17 +- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 16 files changed, 1470 insertions(+), 1539 deletions(-) delete mode 100644 app/src/main/java/hk/uwu/reareye/utils/blend/BlendTokenConfig.kt diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/RearWallpaperHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/RearWallpaperHook.kt index 1a292f1..9a985c4 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/RearWallpaperHook.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/RearWallpaperHook.kt @@ -263,73 +263,73 @@ class RearWallpaperHook : YukiBaseHooker() { YLog.warn(it) } } - val saveSelectionPoint = - resolveMainPanelSaveSelectionMethod(bridge) - resolveLauncherMainPanelFieldName() - resolveLauncherMainHandlerFieldName() - resolveMainPanelSelectMethod() - resolveMainPanelEditModeFieldName() - resolveMainPanelResumedFieldName() - resolveMainPanelAodFieldName() - resolveMainPanelSelectedIndexFieldName() - resolveMainPanelWidgetListFieldName() - resolveWidgetFactoryMethod() - resolveWallpaperSpecIdFieldName() - resolveWallpaperSpecExtrasFieldName() - resolveWidgetIdFieldName() - resolveWidgetSpecFieldName() - resolveWidgetExtrasFieldName() - resolveWidgetHostFieldName() - resolveWidgetPreviewModeFieldName() - resolveWidgetSetEditModeMethod() - resolveWidgetCreateViewMethod() - resolveWidgetSetAodMethod() - resolveWidgetResumeMethod() - resolveWidgetCleanupMethod() - resolvePrefStoreClass() - resolvePrefStoreInstanceFieldName() - resolvePrefStoreLoadSpecsMethod() - resolvePrefStoreReadValueMethod() - resolvePrefStoreWriteValueMethod() - resolveWallpaperRuntimeListMethod() - resolveDeviceConfigClass() - resolveDeviceConfigRenderSizeFieldName() - resolveDeviceConfigLocaleSuffixFieldName() - - launcherRef.firstMethod { - name = "onResume" - parameterCount = 0 - }.hook().after { - runCatching { - capturePanels(instance) - refreshSchedule(forceApply = true) - }.onFailure { - YLog.warn(it) - } - } + val saveSelectionPoint = + resolveMainPanelSaveSelectionMethod(bridge) + resolveLauncherMainPanelFieldName() + resolveLauncherMainHandlerFieldName() + resolveMainPanelSelectMethod() + resolveMainPanelEditModeFieldName() + resolveMainPanelResumedFieldName() + resolveMainPanelAodFieldName() + resolveMainPanelSelectedIndexFieldName() + resolveMainPanelWidgetListFieldName() + resolveWidgetFactoryMethod() + resolveWallpaperSpecIdFieldName() + resolveWallpaperSpecExtrasFieldName() + resolveWidgetIdFieldName() + resolveWidgetSpecFieldName() + resolveWidgetExtrasFieldName() + resolveWidgetHostFieldName() + resolveWidgetPreviewModeFieldName() + resolveWidgetSetEditModeMethod() + resolveWidgetCreateViewMethod() + resolveWidgetSetAodMethod() + resolveWidgetResumeMethod() + resolveWidgetCleanupMethod() + resolvePrefStoreClass() + resolvePrefStoreInstanceFieldName() + resolvePrefStoreLoadSpecsMethod() + resolvePrefStoreReadValueMethod() + resolvePrefStoreWriteValueMethod() + resolveWallpaperRuntimeListMethod() + resolveDeviceConfigClass() + resolveDeviceConfigRenderSizeFieldName() + resolveDeviceConfigLocaleSuffixFieldName() - launcherRef.firstMethod { - name = "onPause" - parameterCount = 0 - }.hook().before { - debugLog("launcher onPause keep scheduler nextAt=${readNextSwitchAt()}") - } + launcherRef.firstMethod { + name = "onResume" + parameterCount = 0 + }.hook().after { + runCatching { + capturePanels(instance) + refreshSchedule(forceApply = true) + }.onFailure { + YLog.warn(it) + } + } - launcherRef.firstMethod { - name = "onDestroy" - parameterCount = 0 - }.hook().before { - stopScheduler() - mainPanel = null - mainHandler = null - } + launcherRef.firstMethod { + name = "onPause" + parameterCount = 0 + }.hook().before { + debugLog("launcher onPause keep scheduler nextAt=${readNextSwitchAt()}") + } - saveSelectionPoint.className.toClass().resolve().firstMethod { - name = saveSelectionPoint.methodName - parameterCount = 0 - }.hook().after { - updateSelectedWallpaperIdFromPanel(instance) - } + launcherRef.firstMethod { + name = "onDestroy" + parameterCount = 0 + }.hook().before { + stopScheduler() + mainPanel = null + mainHandler = null + } + + saveSelectionPoint.className.toClass().resolve().firstMethod { + name = saveSelectionPoint.methodName + parameterCount = 0 + }.hook().after { + updateSelectedWallpaperIdFromPanel(instance) + } } } diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/rearwidget/RearWidgetHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/rearwidget/RearWidgetHook.kt index fdb5aef..b9bf6b8 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/rearwidget/RearWidgetHook.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/modules/rearwidget/RearWidgetHook.kt @@ -2052,14 +2052,14 @@ class RearWidgetHook : YukiBaseHooker() { val extras = RearWidgetRuntimeStore.buildDecoratedExtras(notice.ticket) val runnable = resolveSmartAssistantPostRunnableClassName().toClass().resolve().firstConstructor { - parameterCount = 5 - }.create( - mgr, - notice.ticket.notificationId, - notice.ticket.packageName, - notice.ticket.compositeKey, - extras, - ) as? Runnable ?: return + parameterCount = 5 + }.create( + mgr, + notice.ticket.notificationId, + notice.ticket.packageName, + notice.ticket.compositeKey, + extras, + ) as? Runnable ?: return handler.post(runnable) debugLog("injected ticket key=${notice.ticket.compositeKey} business=${notice.ticket.business}") }.onFailure { diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/modules/UnlockVideoRestrictionsHook.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/modules/UnlockVideoRestrictionsHook.kt index 2e93925..858439a 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/modules/UnlockVideoRestrictionsHook.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/modules/UnlockVideoRestrictionsHook.kt @@ -113,496 +113,496 @@ class UnlockVideoRestrictionsHook : YukiBaseHooker() { val videoEditPoint = resolveVideoEditPlayCreatedMethod(bridge) val fpsLimitPoint = resolveVideoEditFpsLimitMethod(bridge) - val editorConfigBuildPoint = - resolveVideoEditorConfigBuildMethod(bridge) + val editorConfigBuildPoint = + resolveVideoEditorConfigBuildMethod(bridge) val checkDepthPoint = resolveVideoDepthCheckMethod(bridge) - val timelineGetInstancePoint = - resolveVideoTimelineGetInstanceMethod(bridge) - val timelineAttachTexturePoint = - resolveVideoTimelineAttachTextureMethod(bridge) - val timelineGetDurationPoint = - resolveVideoTimelineGetDurationMethod(bridge) - val timelinePreparePoint = - resolveVideoTimelinePrepareMethod(bridge) - val timelineExportPoint = - resolveVideoTimelineExportMethod(bridge) + val timelineGetInstancePoint = + resolveVideoTimelineGetInstanceMethod(bridge) + val timelineAttachTexturePoint = + resolveVideoTimelineAttachTextureMethod(bridge) + val timelineGetDurationPoint = + resolveVideoTimelineGetDurationMethod(bridge) + val timelinePreparePoint = + resolveVideoTimelinePrepareMethod(bridge) + val timelineExportPoint = + resolveVideoTimelineExportMethod(bridge) val toastTextPoint = resolveVideoToastTextMethod(bridge) - val operationCurrentTimePoint = - resolveVideoOperationCurrentTimeMethod(bridge) + val operationCurrentTimePoint = + resolveVideoOperationCurrentTimeMethod(bridge) val clipFrameLoadPoint = resolveVideoClipFrameLoadMethod(bridge) val coderHashPoint = resolveVideoHashStringMethod(bridge) - val exportConfigSetFpsPoint = - resolveVideoExportConfigSetFpsMethod(bridge) + val exportConfigSetFpsPoint = + resolveVideoExportConfigSetFpsMethod(bridge) val gsonSerializePoint = resolveVideoGsonSerializeMethod(bridge) - val videoEditClz = videoEditPoint.className.toClass() - val videoEditRef = videoEditClz.resolve() - val fpsLimitClz = fpsLimitPoint.className.toClass().resolve() - val editorCfgBuilderClz = editorConfigBuildPoint.className.toClass().resolve() - val checkDepthClz = checkDepthPoint.className.toClass().resolve() - val timelineClz = timelineGetInstancePoint.className.toClass() - val timelineRef = timelineClz.resolve() - val toastUtilsRef = toastTextPoint.className.toClass().resolve() - val coderUtilsRef = coderHashPoint.className.toClass().resolve() - val gsonUtilsClz = gsonSerializePoint.className.toClass().resolve() + val videoEditClz = videoEditPoint.className.toClass() + val videoEditRef = videoEditClz.resolve() + val fpsLimitClz = fpsLimitPoint.className.toClass().resolve() + val editorCfgBuilderClz = editorConfigBuildPoint.className.toClass().resolve() + val checkDepthClz = checkDepthPoint.className.toClass().resolve() + val timelineClz = timelineGetInstancePoint.className.toClass() + val timelineRef = timelineClz.resolve() + val toastUtilsRef = toastTextPoint.className.toClass().resolve() + val coderUtilsRef = coderHashPoint.className.toClass().resolve() + val gsonUtilsClz = gsonSerializePoint.className.toClass().resolve() val frameLoaderClassName = resolveDexKitClassValue( - bridge = bridge, - cacheKey = VIDEO_FRAME_LOADER_CLASS_CACHE_KEY, + bridge = bridge, + cacheKey = VIDEO_FRAME_LOADER_CLASS_CACHE_KEY, selector = { it.className.substringBefore('$') }, - ) { - findClass { - matcher { - usingStrings( - "MiVideoFrameLoader", - "loadFrameTime width=%d height=%d key=%s,timeMicros=%d,cost=%d", - ) - } - }.singleOrNull() - } ?: error("DexKit failed to resolve video frame loader class") + ) { + findClass { + matcher { + usingStrings( + "MiVideoFrameLoader", + "loadFrameTime width=%d height=%d key=%s,timeMicros=%d,cost=%d", + ) + } + }.singleOrNull() + } ?: error("DexKit failed to resolve video frame loader class") val exportConfigClassName = resolveDexKitClassValue( - bridge = bridge, - cacheKey = VIDEO_EXPORT_CONFIG_CLASS_CACHE_KEY, - ) { - findClass { - searchPackages("com.android.thememanager.videoedit.entity") - matcher { - fields { - addForType(Int::class.java) - addForType(Size::class.java) - addForType(Boolean::class.java) - addForType(String::class.java) - } - } - }.singleOrNull() - } ?: error("DexKit failed to resolve export config class") - val frameLoaderClz = frameLoaderClassName.toClass().resolve() - val exportConfigClz = exportConfigClassName.toClass().resolve() - - fun resolveFieldName( - cacheKey: String, - fallbackField: String, - finder: DexKitBridge.() -> org.luckypray.dexkit.result.FieldData?, - ): String { - return resolveDexKitFieldValue( - bridge = bridge, - cacheKey = cacheKey, - ) { - finder() - } ?: fallbackField + bridge = bridge, + cacheKey = VIDEO_EXPORT_CONFIG_CLASS_CACHE_KEY, + ) { + findClass { + searchPackages("com.android.thememanager.videoedit.entity") + matcher { + fields { + addForType(Int::class.java) + addForType(Size::class.java) + addForType(Boolean::class.java) + addForType(String::class.java) + } } + }.singleOrNull() + } ?: error("DexKit failed to resolve export config class") + val frameLoaderClz = frameLoaderClassName.toClass().resolve() + val exportConfigClz = exportConfigClassName.toClass().resolve() - val playViewFieldName = resolveFieldName( - VIDEO_EDIT_PLAY_VIEW_FIELD_CACHE_KEY, - "q", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { - declaredClass = videoEditPoint.className - type = "com.android.thememanager.videoedit.widget.VlogPlayView" - } - }.singleOrNull() + fun resolveFieldName( + cacheKey: String, + fallbackField: String, + finder: DexKitBridge.() -> org.luckypray.dexkit.result.FieldData?, + ): String { + return resolveDexKitFieldValue( + bridge = bridge, + cacheKey = cacheKey, + ) { + finder() + } ?: fallbackField + } + + val playViewFieldName = resolveFieldName( + VIDEO_EDIT_PLAY_VIEW_FIELD_CACHE_KEY, + "q", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "com.android.thememanager.videoedit.widget.VlogPlayView" } - val configFieldName = resolveFieldName( - VIDEO_EDIT_CONFIG_FIELD_CACHE_KEY, - "s", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { - declaredClass = videoEditPoint.className - type = "com.android.thememanager.videoedit.VideoEditorConfig" - } - }.singleOrNull() + }.singleOrNull() + } + val configFieldName = resolveFieldName( + VIDEO_EDIT_CONFIG_FIELD_CACHE_KEY, + "s", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "com.android.thememanager.videoedit.VideoEditorConfig" } - val operationViewFieldName = resolveFieldName( - VIDEO_EDIT_OPERATION_VIEW_FIELD_CACHE_KEY, - "n", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { - declaredClass = videoEditPoint.className - type = - "com.android.thememanager.videoedit.widget.SingleEditOperationView" - } - }.singleOrNull() + }.singleOrNull() + } + val operationViewFieldName = resolveFieldName( + VIDEO_EDIT_OPERATION_VIEW_FIELD_CACHE_KEY, + "n", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = + "com.android.thememanager.videoedit.widget.SingleEditOperationView" } - val trimInFieldName = resolveFieldName( - VIDEO_EDIT_TRIM_IN_FIELD_CACHE_KEY, - "i", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { + }.singleOrNull() + } + val trimInFieldName = resolveFieldName( + VIDEO_EDIT_TRIM_IN_FIELD_CACHE_KEY, + "i", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "long" + readMethods { + add { declaredClass = videoEditPoint.className - type = "long" - readMethods { - add { - declaredClass = videoEditPoint.className - name = videoEditPoint.methodName - paramCount(0) - returnType = "void" - usingStrings("onPlayViewCreated") - } - add { - declaredClass = fpsLimitPoint.className - name = fpsLimitPoint.methodName - paramCount(0) - returnType = "void" - usingStrings("ExportConfig %s", "export videopath is ") - } - } + name = videoEditPoint.methodName + paramCount(0) + returnType = "void" + usingStrings("onPlayViewCreated") } - }.singleOrNull() - } - val trimOutFieldName = resolveFieldName( - VIDEO_EDIT_TRIM_OUT_FIELD_CACHE_KEY, - "z", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { - declaredClass = videoEditPoint.className - type = "long" - readMethods { - add { - declaredClass = fpsLimitPoint.className - name = fpsLimitPoint.methodName - paramCount(0) - returnType = "void" - usingStrings("ExportConfig %s", "export videopath is ") - } - } - writeMethods { - add { - declaredClass = videoEditPoint.className - name = videoEditPoint.methodName - paramCount(0) - returnType = "void" - usingStrings("onPlayViewCreated") - } - } + add { + declaredClass = fpsLimitPoint.className + name = fpsLimitPoint.methodName + paramCount(0) + returnType = "void" + usingStrings("ExportConfig %s", "export videopath is ") } - }.singleOrNull() + } } - val frameLoaderFieldName = resolveFieldName( - VIDEO_EDIT_FRAME_LOADER_FIELD_CACHE_KEY, - "p", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { - declaredClass = videoEditPoint.className - type = "com.android.thememanager.videoedit.y" + }.singleOrNull() + } + val trimOutFieldName = resolveFieldName( + VIDEO_EDIT_TRIM_OUT_FIELD_CACHE_KEY, + "z", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "long" + readMethods { + add { + declaredClass = fpsLimitPoint.className + name = fpsLimitPoint.methodName + paramCount(0) + returnType = "void" + usingStrings("ExportConfig %s", "export videopath is ") } - }.singleOrNull() - } - val clipFrameFieldName = resolveFieldName( - VIDEO_EDIT_CLIP_FRAME_FIELD_CACHE_KEY, - "g", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { + } + writeMethods { + add { declaredClass = videoEditPoint.className - type = "com.android.thememanager.videoedit.widget.ClipFrameView" + name = videoEditPoint.methodName + paramCount(0) + returnType = "void" + usingStrings("onPlayViewCreated") } - }.singleOrNull() + } } - val clipListenerFieldName = resolveFieldName( - VIDEO_EDIT_CLIP_LISTENER_FIELD_CACHE_KEY, - "j", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { - declaredClass = videoEditPoint.className - type = - $$"com.android.thememanager.videoedit.widget.ClipFrameView$zy" - } - }.singleOrNull() + }.singleOrNull() + } + val frameLoaderFieldName = resolveFieldName( + VIDEO_EDIT_FRAME_LOADER_FIELD_CACHE_KEY, + "p", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "com.android.thememanager.videoedit.y" + } + }.singleOrNull() + } + val clipFrameFieldName = resolveFieldName( + VIDEO_EDIT_CLIP_FRAME_FIELD_CACHE_KEY, + "g", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "com.android.thememanager.videoedit.widget.ClipFrameView" } - val videoUriFieldName = resolveFieldName( - VIDEO_EDIT_VIDEO_URI_FIELD_CACHE_KEY, - "y", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { + }.singleOrNull() + } + val clipListenerFieldName = resolveFieldName( + VIDEO_EDIT_CLIP_LISTENER_FIELD_CACHE_KEY, + "j", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = + $$"com.android.thememanager.videoedit.widget.ClipFrameView$zy" + } + }.singleOrNull() + } + val videoUriFieldName = resolveFieldName( + VIDEO_EDIT_VIDEO_URI_FIELD_CACHE_KEY, + "y", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "java.lang.String" + readMethods { + add { declaredClass = videoEditPoint.className - type = "java.lang.String" - readMethods { - add { - declaredClass = videoEditPoint.className - name = videoEditPoint.methodName - paramCount(0) - returnType = "void" - usingStrings("onPlayViewCreated") - } - add { - declaredClass = fpsLimitPoint.className - name = fpsLimitPoint.methodName - paramCount(0) - returnType = "void" - usingStrings("ExportConfig %s", "export videopath is ") - } - } + name = videoEditPoint.methodName + paramCount(0) + returnType = "void" + usingStrings("onPlayViewCreated") + } + add { + declaredClass = fpsLimitPoint.className + name = fpsLimitPoint.methodName + paramCount(0) + returnType = "void" + usingStrings("ExportConfig %s", "export videopath is ") } - }.singleOrNull() + } } - val exportPathFieldName = resolveFieldName( - VIDEO_EDIT_EXPORT_PATH_FIELD_CACHE_KEY, - "c", - ) { - findField { - searchPackages("com.android.thememanager.videoedit") - matcher { - declaredClass = videoEditPoint.className - type = "java.lang.String" - writeMethods { - add { - declaredClass = fpsLimitPoint.className - name = fpsLimitPoint.methodName - paramCount(0) - returnType = "void" - usingStrings("ExportConfig %s", "export videopath is ") - } - } + }.singleOrNull() + } + val exportPathFieldName = resolveFieldName( + VIDEO_EDIT_EXPORT_PATH_FIELD_CACHE_KEY, + "c", + ) { + findField { + searchPackages("com.android.thememanager.videoedit") + matcher { + declaredClass = videoEditPoint.className + type = "java.lang.String" + writeMethods { + add { + declaredClass = fpsLimitPoint.className + name = fpsLimitPoint.methodName + paramCount(0) + returnType = "void" + usingStrings("ExportConfig %s", "export videopath is ") } - }.singleOrNull() + } } + }.singleOrNull() + } val durationCropMatchResult = resolveDexKitClassValue( - bridge = bridge, - cacheKey = durationCropCacheKey, - ) { - findClass { - searchPackages("com.android.thememanager.util") - matcher { - modifiers = Modifier.PUBLIC or Modifier.FINAL - fieldCount(1) - methods { - add { - name = "toString" - returnType(String::class.java) - usingStrings("DurationCrop") - } - } + bridge = bridge, + cacheKey = durationCropCacheKey, + ) { + findClass { + searchPackages("com.android.thememanager.util") + matcher { + modifiers = Modifier.PUBLIC or Modifier.FINAL + fieldCount(1) + methods { + add { + name = "toString" + returnType(String::class.java) + usingStrings("DurationCrop") } - }.singleOrNull() + } } - val durationCropClz = (durationCropMatchResult - ?: $$"com.android.thememanager.util.uc$k$toq").toClass() + }.singleOrNull() + } + val durationCropClz = (durationCropMatchResult + ?: $$"com.android.thememanager.util.uc$k$toq").toClass() val historyHelperResult = resolveDexKitClassValue( - bridge = bridge, - cacheKey = historyHelperCacheKey, - ) { - findClass { - searchPackages("com.android.thememanager.settings") - matcher { - modifiers = Modifier.PUBLIC - fields { - addForType(String::class.java) - addForType(Any::class.java) - count = 2 - } - usingStrings("updateVideoResource") - } - }.singleOrNull() - } - val historyHelperClz = - (historyHelperResult ?: "com.android.thememanager.settings.a9").toClass() - - checkDepthClz.firstMethod { - name = checkDepthPoint.methodName - }.hook().after { - if (!prefs.getBoolean( - ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, - true - ) - ) return@after - val ref = instance.asResolver() - val videoCfg = ref.firstField { - name = $$"$videoConfig" - }.get() ?: return@after - if (videoCfg.asResolver().field { - type = Boolean::class.java - }.all { it.get() == true } && !state.load()) { - result = durationCropClz.resolve().firstField { - type = durationCropClz - }.get() - } else { - state.store(false) + bridge = bridge, + cacheKey = historyHelperCacheKey, + ) { + findClass { + searchPackages("com.android.thememanager.settings") + matcher { + modifiers = Modifier.PUBLIC + fields { + addForType(String::class.java) + addForType(Any::class.java) + count = 2 } + usingStrings("updateVideoResource") } + }.singleOrNull() + } + val historyHelperClz = + (historyHelperResult ?: "com.android.thememanager.settings.a9").toClass() - // 修补视频编辑器 - videoEditRef.firstMethod { - name = videoEditPoint.methodName - returnType = Void.TYPE - }.hook().replaceUnit { - if (!prefs.getBoolean(ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, true)) { - invokeOriginal() - return@replaceUnit - } - val iRef = instance.asResolver() - val playViewRef = iRef.firstField { - name = playViewFieldName - }.get()!!.asResolver() - val videoConfig = iRef.firstField { - name = configFieldName - }.get() - val currentTrimIn = iRef.firstField { - name = trimInFieldName - }.get() as? Long ?: 0L - val operationViewRef = iRef.firstField { - name = operationViewFieldName - }.get()!!.asResolver() - val clipFrameRef = iRef.firstField { - name = clipFrameFieldName - }.get()!!.asResolver() - val videoUri = iRef.firstField { - name = videoUriFieldName - }.get() - val sInstance = timelineRef.firstMethod { - name = timelineGetInstancePoint.methodName - returnType = timelineClz - }.invoke()!! - sInstance.asResolver().firstMethod { - name = timelineAttachTexturePoint.methodName - returnType = Void.TYPE - }.invoke(playViewRef.firstMethod { - name = "getTextureView" - }.invoke(), videoConfig) - val duration: Long = - sInstance.asResolver().firstMethod { - name = timelineGetDurationPoint.methodName - }.invoke() as Long - val activity = instance() - if (duration <= 0) { - toastUtilsRef.firstMethod { name = toastTextPoint.methodName } - .invoke(activity.resources.getString(2131888794)) - Log.e("VideoEditActivity", "onPlayViewCreated: originDuration = 0") - activity.finish() - return@replaceUnit - } - iRef.firstField { name = trimOutFieldName }.set(duration) - operationViewRef.firstMethod { name = operationCurrentTimePoint.methodName } - .invoke(currentTrimIn) - operationViewRef.firstMethod { name = "setTotalTime" }.invoke(duration) - val yVar = frameLoaderClz.firstConstructor { - parameterCount = 0 - }.create() - iRef.firstField { name = frameLoaderFieldName }.set(yVar) - clipFrameRef.firstMethod { name = "setVideoFrameLoader" }.invoke(yVar) - clipFrameRef.firstMethod { name = "setClipFrameListener" } - .invoke(iRef.firstField { name = clipListenerFieldName }.get()) - clipFrameRef.firstMethod { name = clipFrameLoadPoint.methodName }.invoke( - videoUri, - duration, - duration - ) - sInstance.asResolver().firstMethod { - name = timelinePreparePoint.methodName - parameters(Int::class.java) - }.invoke(currentTrimIn.toInt()) - state.store(true) - } + checkDepthClz.firstMethod { + name = checkDepthPoint.methodName + }.hook().after { + if (!prefs.getBoolean( + ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, + true + ) + ) return@after + val ref = instance.asResolver() + val videoCfg = ref.firstField { + name = $$"$videoConfig" + }.get() ?: return@after + if (videoCfg.asResolver().field { + type = Boolean::class.java + }.all { it.get() == true } && !state.load()) { + result = durationCropClz.resolve().firstField { + type = durationCropClz + }.get() + } else { + state.store(false) + } + } - // 修补帧率限制 - fpsLimitClz.firstMethod { - name = fpsLimitPoint.methodName - }.hook().replaceUnit { - if (!prefs.getBoolean(ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, true)) { - invokeOriginal() - return@replaceUnit - } - val strF7l8 = - historyHelperClz.resolve().firstMethod { - returnType = String::class.java - parameterCount = 0 - }.invoke() as String - val iVEA = instance.asResolver().firstField { type = videoEditClz }.get()!! - val iRef = iVEA.asResolver() - val yObj = iRef.firstField { name = videoUriFieldName }.get() - val cFieldRef = iRef.firstField { name = exportPathFieldName } - cFieldRef.set( - strF7l8 + (coderUtilsRef.firstMethod { - name = coderHashPoint.methodName - }.invoke(yObj) as String) + ".mp4" - ) - val frameRetriever = - "com.xiaomi.milab.videosdk.FrameRetriever".toClass().resolve() - .firstConstructor().create().asResolver() - frameRetriever.firstMethod { name = "setDataSource" }.invoke(yObj) - val width = frameRetriever.firstMethod { name = "getWidth" }.invoke() as Int - val height = - frameRetriever.firstMethod { name = "getHeight" }.invoke() as Int - val fps = frameRetriever.firstMethod { name = "getFPS" }.invoke() as Float - val bitrate = - frameRetriever.firstMethod { name = "getBitrate" }.invoke() as Long - frameRetriever.firstMethod { name = "release" }.invoke() - if (width <= 0 || height <= 0) { - iRef.firstMethod { name = "onExportFail" }.invoke() - return@replaceUnit - } - val (outWidth, outHeight) = computeExportOutputSize(width, height, 1080) - val toqVar = exportConfigClz.firstConstructor { - parameterCount = 5 - }.create( - true, - cFieldRef.get(), - Size(outWidth, outHeight), - (((((bitrate / (width * height)) * outWidth) * outHeight) / fps) * fps).toInt(), - 0 - ) - toqVar.asResolver().firstMethod { - name = exportConfigSetFpsPoint.methodName - }.invoke(fps.toInt()) - Log.d( - "VideoEditActivity", - String.format( - "ExportConfig %s", - gsonUtilsClz.firstMethod { name = gsonSerializePoint.methodName } - .invoke(toqVar) - ) - ) - Log.d("lollipop", "export videopath is " + cFieldRef.get()) - val qRef = timelineRef.firstMethod { - name = timelineGetInstancePoint.methodName - }.invoke()!!.asResolver() - qRef.firstMethod { - name = timelineExportPoint.methodName - }.invoke( - iRef.firstField { name = trimInFieldName }.get(), - iRef.firstField { name = trimOutFieldName }.get(), - toqVar - ) - } + // 修补视频编辑器 + videoEditRef.firstMethod { + name = videoEditPoint.methodName + returnType = Void.TYPE + }.hook().replaceUnit { + if (!prefs.getBoolean(ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, true)) { + invokeOriginal() + return@replaceUnit + } + val iRef = instance.asResolver() + val playViewRef = iRef.firstField { + name = playViewFieldName + }.get()!!.asResolver() + val videoConfig = iRef.firstField { + name = configFieldName + }.get() + val currentTrimIn = iRef.firstField { + name = trimInFieldName + }.get() as? Long ?: 0L + val operationViewRef = iRef.firstField { + name = operationViewFieldName + }.get()!!.asResolver() + val clipFrameRef = iRef.firstField { + name = clipFrameFieldName + }.get()!!.asResolver() + val videoUri = iRef.firstField { + name = videoUriFieldName + }.get() + val sInstance = timelineRef.firstMethod { + name = timelineGetInstancePoint.methodName + returnType = timelineClz + }.invoke()!! + sInstance.asResolver().firstMethod { + name = timelineAttachTexturePoint.methodName + returnType = Void.TYPE + }.invoke(playViewRef.firstMethod { + name = "getTextureView" + }.invoke(), videoConfig) + val duration: Long = + sInstance.asResolver().firstMethod { + name = timelineGetDurationPoint.methodName + }.invoke() as Long + val activity = instance() + if (duration <= 0) { + toastUtilsRef.firstMethod { name = toastTextPoint.methodName } + .invoke(activity.resources.getString(2131888794)) + Log.e("VideoEditActivity", "onPlayViewCreated: originDuration = 0") + activity.finish() + return@replaceUnit + } + iRef.firstField { name = trimOutFieldName }.set(duration) + operationViewRef.firstMethod { name = operationCurrentTimePoint.methodName } + .invoke(currentTrimIn) + operationViewRef.firstMethod { name = "setTotalTime" }.invoke(duration) + val yVar = frameLoaderClz.firstConstructor { + parameterCount = 0 + }.create() + iRef.firstField { name = frameLoaderFieldName }.set(yVar) + clipFrameRef.firstMethod { name = "setVideoFrameLoader" }.invoke(yVar) + clipFrameRef.firstMethod { name = "setClipFrameListener" } + .invoke(iRef.firstField { name = clipListenerFieldName }.get()) + clipFrameRef.firstMethod { name = clipFrameLoadPoint.methodName }.invoke( + videoUri, + duration, + duration + ) + sInstance.asResolver().firstMethod { + name = timelinePreparePoint.methodName + parameters(Int::class.java) + }.invoke(currentTrimIn.toInt()) + state.store(true) + } - editorCfgBuilderClz.firstMethod { - name = editorConfigBuildPoint.methodName - }.hook().before { - if (!prefs.getBoolean( - ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, - true - ) - ) return@before - val ref = instance.asResolver() - val isCallFromRearScreen = ref.field { - type = Boolean::class.java - }.all { it.get() == true } - if (isCallFromRearScreen) { - YLog.debug("Overwriting video editor max duration & frame-rate limitations") - ref.firstField { - type = Long::class.java - }.set(Long.MAX_VALUE) - ref.firstField { - type = Int::class.java - }.set(120) - } - } + // 修补帧率限制 + fpsLimitClz.firstMethod { + name = fpsLimitPoint.methodName + }.hook().replaceUnit { + if (!prefs.getBoolean(ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, true)) { + invokeOriginal() + return@replaceUnit + } + val strF7l8 = + historyHelperClz.resolve().firstMethod { + returnType = String::class.java + parameterCount = 0 + }.invoke() as String + val iVEA = instance.asResolver().firstField { type = videoEditClz }.get()!! + val iRef = iVEA.asResolver() + val yObj = iRef.firstField { name = videoUriFieldName }.get() + val cFieldRef = iRef.firstField { name = exportPathFieldName } + cFieldRef.set( + strF7l8 + (coderUtilsRef.firstMethod { + name = coderHashPoint.methodName + }.invoke(yObj) as String) + ".mp4" + ) + val frameRetriever = + "com.xiaomi.milab.videosdk.FrameRetriever".toClass().resolve() + .firstConstructor().create().asResolver() + frameRetriever.firstMethod { name = "setDataSource" }.invoke(yObj) + val width = frameRetriever.firstMethod { name = "getWidth" }.invoke() as Int + val height = + frameRetriever.firstMethod { name = "getHeight" }.invoke() as Int + val fps = frameRetriever.firstMethod { name = "getFPS" }.invoke() as Float + val bitrate = + frameRetriever.firstMethod { name = "getBitrate" }.invoke() as Long + frameRetriever.firstMethod { name = "release" }.invoke() + if (width <= 0 || height <= 0) { + iRef.firstMethod { name = "onExportFail" }.invoke() + return@replaceUnit + } + val (outWidth, outHeight) = computeExportOutputSize(width, height, 1080) + val toqVar = exportConfigClz.firstConstructor { + parameterCount = 5 + }.create( + true, + cFieldRef.get(), + Size(outWidth, outHeight), + (((((bitrate / (width * height)) * outWidth) * outHeight) / fps) * fps).toInt(), + 0 + ) + toqVar.asResolver().firstMethod { + name = exportConfigSetFpsPoint.methodName + }.invoke(fps.toInt()) + Log.d( + "VideoEditActivity", + String.format( + "ExportConfig %s", + gsonUtilsClz.firstMethod { name = gsonSerializePoint.methodName } + .invoke(toqVar) + ) + ) + Log.d("lollipop", "export videopath is " + cFieldRef.get()) + val qRef = timelineRef.firstMethod { + name = timelineGetInstancePoint.methodName + }.invoke()!!.asResolver() + qRef.firstMethod { + name = timelineExportPoint.methodName + }.invoke( + iRef.firstField { name = trimInFieldName }.get(), + iRef.firstField { name = trimOutFieldName }.get(), + toqVar + ) + } + + editorCfgBuilderClz.firstMethod { + name = editorConfigBuildPoint.methodName + }.hook().before { + if (!prefs.getBoolean( + ConfigKeys.HOOK_UNLOCK_VIDEO_RESTRICTIONS, + true + ) + ) return@before + val ref = instance.asResolver() + val isCallFromRearScreen = ref.field { + type = Boolean::class.java + }.all { it.get() == true } + if (isCallFromRearScreen) { + YLog.debug("Overwriting video editor max duration & frame-rate limitations") + ref.firstField { + type = Long::class.java + }.set(Long.MAX_VALUE) + ref.firstField { + type = Int::class.java + }.set(120) + } + } } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt index 79960df..9e469d1 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/CardManagerScreen.kt @@ -339,325 +339,328 @@ fun CardManagerScreen( }, ) { Scaffold( - topBar = { - TopAppBar( - modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), - color = Color.Transparent, - title = stringResource(R.string.rear_widget_card_manager), - navigationIconPadding = 12.dp, - actionIconPadding = 12.dp, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - modifier = Modifier.graphicsLayer { - if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f - }, - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - ) - } - }, - actions = { - IconButton( - onClick = { if (cardsLoaded) openCreateDialog() }) { - Icon(imageVector = Icons.Filled.Add, contentDescription = null) - } - }, - scrollBehavior = scrollBehavior, - ) - }, - ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .scrollEndHaptic() - .overScrollVertical() - .rearAcrylicSource(hazeState) - .padding(horizontal = 12.dp), - contentPadding = PaddingValues( - top = paddingValues.calculateTopPadding() + 12.dp, - bottom = paddingValues.calculateBottomPadding() + 12.dp, - ), - verticalArrangement = Arrangement.spacedBy(8.dp), - overscrollEffect = null, - ) { - item { - Card( - modifier = Modifier - .padding(bottom = 12.dp) - .fillMaxWidth() - ) { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - SuperCard( - title = stringResource(R.string.rear_widget_card_dialog_hint_title), - summary = stringResource(R.string.rear_widget_card_dialog_hint), - onClick = {}, - bottomAction = { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - if (cardsLoaded) { - RearBadgeGroup( - badges = listOf(rearWidgetCardCountBadge(cards.size)), - ) - } - Button( - onClick = { openCreateDialog() }, - colors = ButtonDefaults.buttonColorsPrimary(), - modifier = Modifier.fillMaxWidth(), - ) { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = null, - modifier = Modifier.padding(end = 6.dp), - ) - Text(text = stringResource(R.string.rear_widget_add_card)) + topBar = { + TopAppBar( + modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), + color = Color.Transparent, + title = stringResource(R.string.rear_widget_card_manager), + navigationIconPadding = 12.dp, + actionIconPadding = 12.dp, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + modifier = Modifier.graphicsLayer { + if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f + }, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + actions = { + IconButton( + onClick = { if (cardsLoaded) openCreateDialog() }) { + Icon(imageVector = Icons.Filled.Add, contentDescription = null) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .scrollEndHaptic() + .overScrollVertical() + .rearAcrylicSource(hazeState) + .padding(horizontal = 12.dp), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding() + 12.dp, + bottom = paddingValues.calculateBottomPadding() + 12.dp, + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + overscrollEffect = null, + ) { + item { + Card( + modifier = Modifier + .padding(bottom = 12.dp) + .fillMaxWidth() + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + SuperCard( + title = stringResource(R.string.rear_widget_card_dialog_hint_title), + summary = stringResource(R.string.rear_widget_card_dialog_hint), + onClick = {}, + bottomAction = { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + if (cardsLoaded) { + RearBadgeGroup( + badges = listOf(rearWidgetCardCountBadge(cards.size)), + ) + } + Button( + onClick = { openCreateDialog() }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null, + modifier = Modifier.padding(end = 6.dp), + ) + Text(text = stringResource(R.string.rear_widget_add_card)) + } } } - } - ) + ) + } } } - } - if (!dataCardsVisible) { - item { - Card( - modifier = Modifier.fillMaxWidth(), - insideMargin = PaddingValues(vertical = 24.dp), - ) { - Box( + if (!dataCardsVisible) { + item { + Card( modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, + insideMargin = PaddingValues(vertical = 24.dp), ) { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, ) { - InfiniteProgressIndicator() - Text(text = stringResource(R.string.rear_widget_loading_data)) + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + InfiniteProgressIndicator() + Text(text = stringResource(R.string.rear_widget_loading_data)) + } } } } } - } - if (dataCardsVisible) { - itemsIndexed( - items = cards, - key = { _, item -> item.id }, - contentType = { _, _ -> "card_item" }, - ) { _, item -> - val normalizedBusiness = normalizeTemplateBusinessName(item.business) - val hasTemplateConfig = - templateAvailability[item.business] == true || - templateAvailability[normalizedBusiness] == true || - item.oneConfigJson.isNullOrBlank().not() - if (prefsManager.getBoolean(ConfigKeys.MORE_DEBUG, false)) { - debugLog( - "card template action id=${item.id} business=${item.business} normalized=$normalizedBusiness available=$hasTemplateConfig rawAvailable=${templateAvailability[item.business]} normalizedAvailable=${templateAvailability[normalizedBusiness]} hasConfig=${ - item.oneConfigJson.isNullOrBlank().not() - }" - ) - } - ModuleStyleManagerCard( - title = item.title, - badges = buildList { - add(rearWidgetPackageBadge(item.packageName)) - add(rearWidgetBusinessBadge(item.business)) - add(rearWidgetPriorityBadge(item.priority)) - if (item.sticky) { - add(rearWidgetStickyBadge()) - } - if (!item.renameable) { - add(rearWidgetLockedBadge()) - } - add( - rearWidgetTemplateStatusBadge( - hasCustomConfig = item.oneConfigJson.isNullOrBlank().not(), - ) + if (dataCardsVisible) { + itemsIndexed( + items = cards, + key = { _, item -> item.id }, + contentType = { _, _ -> "card_item" }, + ) { _, item -> + val normalizedBusiness = normalizeTemplateBusinessName(item.business) + val hasTemplateConfig = + templateAvailability[item.business] == true || + templateAvailability[normalizedBusiness] == true || + item.oneConfigJson.isNullOrBlank().not() + if (prefsManager.getBoolean(ConfigKeys.MORE_DEBUG, false)) { + debugLog( + "card template action id=${item.id} business=${item.business} normalized=$normalizedBusiness available=$hasTemplateConfig rawAvailable=${templateAvailability[item.business]} normalizedAvailable=${templateAvailability[normalizedBusiness]} hasConfig=${ + item.oneConfigJson.isNullOrBlank().not() + }" ) - addAll( - rearWidgetSourceBadges( - downloadedFromStore = item.downloadedFromStore, - storeWidgetId = item.storeWidgetId, - ) - ) - }, - summaryLines = emptyList(), - trailing = { - if (item.renameable) { - Switch( - checked = item.enabled, - onCheckedChange = { checked -> - val i = cards.indexOfFirst { it.id == item.id } - if (i >= 0) { - cards[i] = cards[i].copy(enabled = checked) - scope.launch(Dispatchers.IO) { - RearWidgetManagerRepository.setCardEnabled( - context = context, - prefsManager = prefsManager, - cardId = item.id, - enabled = checked, - ) - } - } - }, - ) - } else { - Icon( - imageVector = Icons.Outlined.Lock, - contentDescription = null, - tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, + } + ModuleStyleManagerCard( + title = item.title, + badges = buildList { + add(rearWidgetPackageBadge(item.packageName)) + add(rearWidgetBusinessBadge(item.business)) + add(rearWidgetPriorityBadge(item.priority)) + if (item.sticky) { + add(rearWidgetStickyBadge()) + } + if (!item.renameable) { + add(rearWidgetLockedBadge()) + } + add( + rearWidgetTemplateStatusBadge( + hasCustomConfig = item.oneConfigJson.isNullOrBlank().not(), + ) ) - } - }, - onCardClick = { openEditDialog(item) }, - leftAction = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - ModuleStyleIconAction( - icon = Icons.Rounded.EditNote, - onClick = { openEditDialog(item) }, + addAll( + rearWidgetSourceBadges( + downloadedFromStore = item.downloadedFromStore, + storeWidgetId = item.storeWidgetId, + ) ) - if (hasTemplateConfig) { + }, + summaryLines = emptyList(), + trailing = { + if (item.renameable) { + Switch( + checked = item.enabled, + onCheckedChange = { checked -> + val i = cards.indexOfFirst { it.id == item.id } + if (i >= 0) { + cards[i] = cards[i].copy(enabled = checked) + scope.launch(Dispatchers.IO) { + RearWidgetManagerRepository.setCardEnabled( + context = context, + prefsManager = prefsManager, + cardId = item.id, + enabled = checked, + ) + } + } + }, + ) + } else { + Icon( + imageVector = Icons.Outlined.Lock, + contentDescription = null, + tint = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + } + }, + onCardClick = { openEditDialog(item) }, + leftAction = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ModuleStyleIconAction( + icon = Icons.Rounded.EditNote, + onClick = { openEditDialog(item) }, + ) + if (hasTemplateConfig) { + ModuleStyleDeleteAction( + icon = Icons.Filled.Tune, + text = stringResource(R.string.rear_widget_action_config), + onClick = { openTemplateConfig(item) }, + ) + } + } + }, + rightAction = { + if (item.renameable) { ModuleStyleDeleteAction( - icon = Icons.Filled.Tune, - text = stringResource(R.string.rear_widget_action_config), - onClick = { openTemplateConfig(item) }, + icon = MiuixIcons.Delete, + text = stringResource(R.string.rear_widget_action_delete), + onClick = { + if (!item.renameable) return@ModuleStyleDeleteAction + cards.remove(item) + persist() + }, ) } - } - }, - rightAction = { - if (item.renameable) { - ModuleStyleDeleteAction( - icon = MiuixIcons.Delete, - text = stringResource(R.string.rear_widget_action_delete), - onClick = { - if (!item.renameable) return@ModuleStyleDeleteAction - cards.remove(item) - persist() - }, + }, + ) + } + } + + item { + if (dataCardsVisible && cards.isEmpty()) { + ArtRevealItem(visible = true, delayMillis = 40) { + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.rear_widget_empty_card), + modifier = Modifier.padding(16.dp), ) } - }, - ) + } + } } } + } - item { - if (dataCardsVisible && cards.isEmpty()) { - ArtRevealItem(visible = true, delayMillis = 40) { - Card(modifier = Modifier.fillMaxWidth()) { + OverlayDialog( + show = showDialog.value, + title = stringResource( + if (editingCardId == null) R.string.rear_widget_add_card else R.string.rear_widget_edit_card, + ), + onDismissRequest = { showDialog.value = false }, + ) { + val editingCard = editingCardId?.let { id -> cards.firstOrNull { it.id == id } } + val lockedCard = editingCard?.takeIf { !it.renameable } + DialogFormColumn { + if (lockedCard == null) { + TextField( + value = draftTitle, + onValueChange = { draftTitle = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.rear_widget_card_title), + singleLine = true, + ) + TextField( + value = draftPackageName, + onValueChange = { draftPackageName = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.rear_widget_target_package), + singleLine = true, + ) + TextField( + value = draftBusiness, + onValueChange = { draftBusiness = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.rear_widget_business_name), + singleLine = true, + ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResource(R.string.rear_widget_card_locked_summary), + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) Text( - text = stringResource(R.string.rear_widget_empty_card), - modifier = Modifier.padding(16.dp), + text = stringResource( + R.string.rear_widget_card_summary, + lockedCard.packageName, + lockedCard.business, + lockedCard.priority, + ), + fontSize = 12.sp, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, ) } } } - } - } - } - - OverlayDialog( - show = showDialog.value, - title = stringResource( - if (editingCardId == null) R.string.rear_widget_add_card else R.string.rear_widget_edit_card, - ), - onDismissRequest = { showDialog.value = false }, - ) { - val editingCard = editingCardId?.let { id -> cards.firstOrNull { it.id == id } } - val lockedCard = editingCard?.takeIf { !it.renameable } - DialogFormColumn { - if (lockedCard == null) { - TextField( - value = draftTitle, - onValueChange = { draftTitle = it }, - modifier = Modifier.fillMaxWidth(), - label = stringResource(R.string.rear_widget_card_title), - singleLine = true, - ) TextField( - value = draftPackageName, - onValueChange = { draftPackageName = it }, + value = draftPriorityText, + onValueChange = { draftPriorityText = it }, modifier = Modifier.fillMaxWidth(), - label = stringResource(R.string.rear_widget_target_package), + label = stringResource(R.string.rear_widget_default_priority), singleLine = true, ) - TextField( - value = draftBusiness, - onValueChange = { draftBusiness = it }, + if (lockedCard == null) { + SwitchPreference( + title = stringResource(R.string.rear_widget_card_sticky), + summary = stringResource(R.string.rear_widget_card_sticky_desc), + checked = draftSticky, + onCheckedChange = { draftSticky = it }, + ) + } + Column( modifier = Modifier.fillMaxWidth(), - label = stringResource(R.string.rear_widget_business_name), - singleLine = true, - ) - } else { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 2.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp), + Button( + onClick = { submitDialog() }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), ) { - Text( - text = stringResource(R.string.rear_widget_card_locked_summary), - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringResource( - R.string.rear_widget_card_summary, - lockedCard.packageName, - lockedCard.business, - lockedCard.priority, - ), - fontSize = 12.sp, - color = MiuixTheme.colorScheme.onSurfaceVariantSummary, - ) + Text(stringResource(R.string.rear_widget_confirm)) + } + Button( + onClick = { showDialog.value = false }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.rear_widget_cancel)) } - } - } - TextField( - value = draftPriorityText, - onValueChange = { draftPriorityText = it }, - modifier = Modifier.fillMaxWidth(), - label = stringResource(R.string.rear_widget_default_priority), - singleLine = true, - ) - if (lockedCard == null) { - SwitchPreference( - title = stringResource(R.string.rear_widget_card_sticky), - summary = stringResource(R.string.rear_widget_card_sticky_desc), - checked = draftSticky, - onCheckedChange = { draftSticky = it }, - ) - } - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button( - onClick = { submitDialog() }, - colors = ButtonDefaults.buttonColorsPrimary(), - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.rear_widget_confirm)) - } - Button(onClick = { showDialog.value = false }, modifier = Modifier.fillMaxWidth()) { - Text(stringResource(R.string.rear_widget_cancel)) } } } - } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt index f70f96e..b49f920 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/RearWallpaperManagerScreen.kt @@ -488,546 +488,553 @@ fun RearWallpaperManagerScreen( }, ) { Scaffold( - topBar = { - TopAppBar( - modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), - color = Color.Transparent, - title = stringResource( - if (activePage == RearWallpaperPage.MANAGEMENT) { - R.string.rear_wallpaper_manage_title - } else { - R.string.rear_wallpaper_manager - } - ), - navigationIconPadding = 12.dp, - actionIconPadding = 12.dp, - navigationIcon = { - IconButton( - onClick = { - if (activePage == RearWallpaperPage.MANAGEMENT) { - activePage = RearWallpaperPage.ROTATION - } else { - onBack() - } - }, - ) { - Icon( - modifier = Modifier.graphicsLayer { - if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f - }, - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - ) - } - }, - actions = { - if (activePage == RearWallpaperPage.ROTATION) { + topBar = { + TopAppBar( + modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), + color = Color.Transparent, + title = stringResource( + if (activePage == RearWallpaperPage.MANAGEMENT) { + R.string.rear_wallpaper_manage_title + } else { + R.string.rear_wallpaper_manager + } + ), + navigationIconPadding = 12.dp, + actionIconPadding = 12.dp, + navigationIcon = { IconButton( - onClick = { clearAllRotatingWallpapers() }, + onClick = { + if (activePage == RearWallpaperPage.MANAGEMENT) { + activePage = RearWallpaperPage.ROTATION + } else { + onBack() + } + }, ) { Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = stringResource(R.string.rear_wallpaper_clear_schedule), + modifier = Modifier.graphicsLayer { + if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f + }, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, ) } - } - IconButton( - onClick = { if (!refreshing) refreshCatalog(showSuccessToast = true) }, - ) { - Icon(imageVector = Icons.Rounded.Refresh, contentDescription = null) - } - }, - scrollBehavior = scrollBehavior, - ) - }, - ) { paddingValues -> - AnimatedContent( - modifier = Modifier - .fillMaxSize() - .graphicsLayer { clip = true }, - targetState = activePage, - contentKey = { it }, - transitionSpec = { - val forward = targetState == RearWallpaperPage.MANAGEMENT - - fadeIn( - animationSpec = tween( - durationMillis = 210, - delayMillis = 50, - easing = LinearOutSlowInEasing, - ) - ) + slideInHorizontally( - animationSpec = tween( - durationMillis = 280, - easing = FastOutSlowInEasing, - ) - ) { fullWidth -> - if (forward) fullWidth / 9 else -fullWidth / 9 - } togetherWith ( - fadeOut( - animationSpec = tween( - durationMillis = 110, - easing = FastOutLinearInEasing, - ) - ) + slideOutHorizontally( - animationSpec = tween( - durationMillis = 190, - easing = FastOutLinearInEasing, - ) - ) { fullWidth -> - if (forward) -fullWidth / 12 else fullWidth / 12 + }, + actions = { + if (activePage == RearWallpaperPage.ROTATION) { + IconButton( + onClick = { clearAllRotatingWallpapers() }, + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(R.string.rear_wallpaper_clear_schedule), + ) + } } - ) + IconButton( + onClick = { if (!refreshing) refreshCatalog(showSuccessToast = true) }, + ) { + Icon(imageVector = Icons.Rounded.Refresh, contentDescription = null) + } + }, + scrollBehavior = scrollBehavior, + ) }, - label = "RearWallpaperPageTransition", - ) { currentPage -> - when (currentPage) { - RearWallpaperPage.MANAGEMENT -> { - RearWallpaperManagementContent( - paddingValues = paddingValues, - scrollBehavior = scrollBehavior, - hazeState = hazeState, - wallpapers = wallpapers, - storeWallpaperSources = storeWallpaperSources, - currentWallpaperId = currentWallpaperId, - loading = loading, - refreshing = refreshing, - onRefresh = { if (!refreshing) refreshCatalog(showSuccessToast = true) }, - onSetCurrent = ::switchWallpaper, - onImport = ::importWallpaperPackage, - onUpdateMetadata = ::updateWallpaperMetadata, - onEditTemplate = { activeTemplateWallpaperId = it.wallpaperId }, - onGeneratePreview = ::generateWallpaperPreview, - onDelete = ::deleteWallpaper, - ) - } - - RearWallpaperPage.ROTATION -> { - Box( - modifier = Modifier - .fillMaxSize() - .onGloballyPositioned { coordinates -> - contentTopInRoot = coordinates.positionInRoot().y + ) { paddingValues -> + AnimatedContent( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { clip = true }, + targetState = activePage, + contentKey = { it }, + transitionSpec = { + val forward = targetState == RearWallpaperPage.MANAGEMENT + + fadeIn( + animationSpec = tween( + durationMillis = 210, + delayMillis = 50, + easing = LinearOutSlowInEasing, + ) + ) + slideInHorizontally( + animationSpec = tween( + durationMillis = 280, + easing = FastOutSlowInEasing, + ) + ) { fullWidth -> + if (forward) fullWidth / 9 else -fullWidth / 9 + } togetherWith ( + fadeOut( + animationSpec = tween( + durationMillis = 110, + easing = FastOutLinearInEasing, + ) + ) + slideOutHorizontally( + animationSpec = tween( + durationMillis = 190, + easing = FastOutLinearInEasing, + ) + ) { fullWidth -> + if (forward) -fullWidth / 12 else fullWidth / 12 } - ) { - LazyColumn( + ) + }, + label = "RearWallpaperPageTransition", + ) { currentPage -> + when (currentPage) { + RearWallpaperPage.MANAGEMENT -> { + RearWallpaperManagementContent( + paddingValues = paddingValues, + scrollBehavior = scrollBehavior, + hazeState = hazeState, + wallpapers = wallpapers, + storeWallpaperSources = storeWallpaperSources, + currentWallpaperId = currentWallpaperId, + loading = loading, + refreshing = refreshing, + onRefresh = { if (!refreshing) refreshCatalog(showSuccessToast = true) }, + onSetCurrent = ::switchWallpaper, + onImport = ::importWallpaperPackage, + onUpdateMetadata = ::updateWallpaperMetadata, + onEditTemplate = { activeTemplateWallpaperId = it.wallpaperId }, + onGeneratePreview = ::generateWallpaperPreview, + onDelete = ::deleteWallpaper, + ) + } + + RearWallpaperPage.ROTATION -> { + Box( modifier = Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .scrollEndHaptic() - .overScrollVertical() - .rearAcrylicSource(hazeState) - .padding(horizontal = 12.dp), - state = listState, - contentPadding = PaddingValues( - top = paddingValues.calculateTopPadding() + 12.dp, - bottom = paddingValues.calculateBottomPadding() + 12.dp, - ), - verticalArrangement = Arrangement.spacedBy(8.dp), - overscrollEffect = null, - userScrollEnabled = draggedId == null, + .onGloballyPositioned { coordinates -> + contentTopInRoot = coordinates.positionInRoot().y + } ) { - item { - Card(modifier = Modifier.fillMaxWidth()) { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - SuperCard( - title = stringResource(R.string.rear_wallpaper_status_title), - onClick = {}, - bottomAction = { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - RearBadgeGroup(badges = statusBadges) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy( - 8.dp - ), + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .scrollEndHaptic() + .overScrollVertical() + .rearAcrylicSource(hazeState) + .padding(horizontal = 12.dp), + state = listState, + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding() + 12.dp, + bottom = paddingValues.calculateBottomPadding() + 12.dp, + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + overscrollEffect = null, + userScrollEnabled = draggedId == null, + ) { + item { + Card(modifier = Modifier.fillMaxWidth()) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + SuperCard( + title = stringResource(R.string.rear_wallpaper_status_title), + onClick = {}, + bottomAction = { + Column( + verticalArrangement = Arrangement.spacedBy( + 10.dp + ) ) { - Button( - onClick = { - pickerMode.value = - WallpaperPickerMode.ADD_TO_SCHEDULE - }, - colors = ButtonDefaults.buttonColorsPrimary(), - modifier = Modifier.weight(1f), - ) { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = null, - modifier = Modifier.padding(end = 6.dp), - ) - Text(stringResource(R.string.rear_wallpaper_add_sheet_trigger)) - } - Button( - onClick = { - activePage = - RearWallpaperPage.MANAGEMENT - }, - modifier = Modifier.weight(1f), + RearBadgeGroup(badges = statusBadges) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + 8.dp + ), ) { - Text(stringResource(R.string.rear_wallpaper_manage_title)) + Button( + onClick = { + pickerMode.value = + WallpaperPickerMode.ADD_TO_SCHEDULE + }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.weight(1f), + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null, + modifier = Modifier.padding(end = 6.dp), + ) + Text(stringResource(R.string.rear_wallpaper_add_sheet_trigger)) + } + Button( + onClick = { + activePage = + RearWallpaperPage.MANAGEMENT + }, + modifier = Modifier.weight(1f), + ) { + Text(stringResource(R.string.rear_wallpaper_manage_title)) + } } } - } - }, - ) - } - } - } - - item { - Card(modifier = Modifier.fillMaxWidth()) { - BasicComponent( - title = stringResource(R.string.rear_wallpaper_schedule_feature_title), - summary = stringResource(R.string.rear_wallpaper_schedule_hint), - summaryColor = BasicComponentDefaults.summaryColor(), - onClick = {}, - endActions = { - Switch( - checked = scheduleEnabled, - onCheckedChange = { checked -> - if (checked && schedule.isEmpty()) { - toast(R.string.rear_wallpaper_schedule_empty) - } else { - scheduleEnabled = checked - persistSchedule() - } }, ) - }, - ) + } + } } - } - item { - Card(modifier = Modifier.fillMaxWidth()) { - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + item { + Card(modifier = Modifier.fillMaxWidth()) { BasicComponent( - title = stringResource(R.string.rear_wallpaper_schedule_title), - summary = if (schedule.isEmpty()) { - stringResource(R.string.rear_wallpaper_schedule_empty) - } else { - stringResource(R.string.rear_wallpaper_schedule_order_hint) - }, + title = stringResource(R.string.rear_wallpaper_schedule_feature_title), + summary = stringResource(R.string.rear_wallpaper_schedule_hint), summaryColor = BasicComponentDefaults.summaryColor(), onClick = {}, + endActions = { + Switch( + checked = scheduleEnabled, + onCheckedChange = { checked -> + if (checked && schedule.isEmpty()) { + toast(R.string.rear_wallpaper_schedule_empty) + } else { + scheduleEnabled = checked + persistSchedule() + } + }, + ) + }, ) } } - } - if (schedule.isNotEmpty()) { - items(renderedSchedule, key = { it.wallpaperId }) { entry -> - val wallpaper = wallpaperMap[entry.wallpaperId] - val isDragged = draggedId == entry.wallpaperId - val isSettling = settlingWallpaperId == entry.wallpaperId - Box( - modifier = Modifier - .fillMaxWidth() - .then( - if (isDragged || isSettling) { - Modifier + item { + Card(modifier = Modifier.fillMaxWidth()) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + BasicComponent( + title = stringResource(R.string.rear_wallpaper_schedule_title), + summary = if (schedule.isEmpty()) { + stringResource(R.string.rear_wallpaper_schedule_empty) } else { - Modifier.animateItem( - placementSpec = tween( - durationMillis = SETTLING_OVERLAY_DURATION_MS.toInt(), - easing = FastOutSlowInEasing, - ) - ) - } + stringResource(R.string.rear_wallpaper_schedule_order_hint) + }, + summaryColor = BasicComponentDefaults.summaryColor(), + onClick = {}, ) - .zIndex(if (isDragged) 1f else 0f) - .onGloballyPositioned { coordinates -> - scheduleItemBounds[entry.wallpaperId] = ItemBounds( - top = coordinates.positionInRoot().y, - height = coordinates.size.height.toFloat(), + } + } + } + + if (schedule.isNotEmpty()) { + items(renderedSchedule, key = { it.wallpaperId }) { entry -> + val wallpaper = wallpaperMap[entry.wallpaperId] + val isDragged = draggedId == entry.wallpaperId + val isSettling = settlingWallpaperId == entry.wallpaperId + Box( + modifier = Modifier + .fillMaxWidth() + .then( + if (isDragged || isSettling) { + Modifier + } else { + Modifier.animateItem( + placementSpec = tween( + durationMillis = SETTLING_OVERLAY_DURATION_MS.toInt(), + easing = FastOutSlowInEasing, + ) + ) + } ) - } - .pointerInput(entry.wallpaperId) { - detectDragGesturesAfterLongPress( - onDragStart = { - settlingOverlay = null - settlingWallpaperId = null - draggedId = entry.wallpaperId - val bounds = - scheduleItemBounds[entry.wallpaperId] - draggedInsertIndex = - schedule.indexOfFirst { it.wallpaperId == entry.wallpaperId } - draggedStartTop = bounds?.top ?: 0f - draggedItemHeight = bounds?.height ?: 0f - draggedOffsetY = 0f - }, - onDragCancel = { - draggedId = null - draggedInsertIndex = null - draggedItemHeight = 0f - draggedOffsetY = 0f - }, - onDragEnd = { - val draggingId = draggedId - val finalSchedule = previewScheduleEntries( - schedule = schedule, - draggingId = draggingId, - insertIndex = draggedInsertIndex, + .zIndex(if (isDragged) 1f else 0f) + .onGloballyPositioned { coordinates -> + scheduleItemBounds[entry.wallpaperId] = + ItemBounds( + top = coordinates.positionInRoot().y, + height = coordinates.size.height.toFloat(), ) - val draggedEntrySnapshot = - draggingId?.let { wallpaperId -> - schedule.firstOrNull { it.wallpaperId == wallpaperId } - } - val startTranslationY = - draggedStartTop - contentTopInRoot + draggedOffsetY - if (draggedEntrySnapshot != null) { - settlingOverlay = - SettlingScheduleOverlay( - entry = draggedEntrySnapshot, - isCurrent = currentWallpaperId == draggedEntrySnapshot.wallpaperId, - startTranslationY = startTranslationY, - targetTranslationY = startTranslationY, + } + .pointerInput(entry.wallpaperId) { + detectDragGesturesAfterLongPress( + onDragStart = { + settlingOverlay = null + settlingWallpaperId = null + draggedId = entry.wallpaperId + val bounds = + scheduleItemBounds[entry.wallpaperId] + draggedInsertIndex = + schedule.indexOfFirst { it.wallpaperId == entry.wallpaperId } + draggedStartTop = bounds?.top ?: 0f + draggedItemHeight = bounds?.height ?: 0f + draggedOffsetY = 0f + }, + onDragCancel = { + draggedId = null + draggedInsertIndex = null + draggedItemHeight = 0f + draggedOffsetY = 0f + }, + onDragEnd = { + val draggingId = draggedId + val finalSchedule = + previewScheduleEntries( + schedule = schedule, + draggingId = draggingId, + insertIndex = draggedInsertIndex, ) - settlingWallpaperId = - draggedEntrySnapshot.wallpaperId - } - if (finalSchedule.map { it.wallpaperId } != schedule.map { it.wallpaperId }) { - schedule.clear() - schedule.addAll(finalSchedule) - persistSchedule() - } - draggedId = null - draggedInsertIndex = null - draggedItemHeight = 0f - draggedOffsetY = 0f - - if (draggingId != null) { - scope.launch { - withFrameNanos { } - withFrameNanos { } - val resolvedTarget = - scheduleItemBounds[draggingId]?.top?.minus( - contentTopInRoot + val draggedEntrySnapshot = + draggingId?.let { wallpaperId -> + schedule.firstOrNull { it.wallpaperId == wallpaperId } + } + val startTranslationY = + draggedStartTop - contentTopInRoot + draggedOffsetY + if (draggedEntrySnapshot != null) { + settlingOverlay = + SettlingScheduleOverlay( + entry = draggedEntrySnapshot, + isCurrent = currentWallpaperId == draggedEntrySnapshot.wallpaperId, + startTranslationY = startTranslationY, + targetTranslationY = startTranslationY, ) - if (resolvedTarget != null && settlingOverlay?.entry?.wallpaperId == draggingId) { - settlingOverlay = - settlingOverlay?.copy( - targetTranslationY = resolvedTarget + settlingWallpaperId = + draggedEntrySnapshot.wallpaperId + } + if (finalSchedule.map { it.wallpaperId } != schedule.map { it.wallpaperId }) { + schedule.clear() + schedule.addAll(finalSchedule) + persistSchedule() + } + draggedId = null + draggedInsertIndex = null + draggedItemHeight = 0f + draggedOffsetY = 0f + + if (draggingId != null) { + scope.launch { + withFrameNanos { } + withFrameNanos { } + val resolvedTarget = + scheduleItemBounds[draggingId]?.top?.minus( + contentTopInRoot ) + if (resolvedTarget != null && settlingOverlay?.entry?.wallpaperId == draggingId) { + settlingOverlay = + settlingOverlay?.copy( + targetTranslationY = resolvedTarget + ) + } } } - } - }, - onDrag = { change, dragAmount -> - change.consume() - val draggingId = - draggedId - ?: return@detectDragGesturesAfterLongPress - val currentHeight = - scheduleItemBounds[draggingId]?.height - ?: draggedItemHeight - draggedOffsetY += dragAmount.y - val draggedCenter = - draggedStartTop + draggedOffsetY + currentHeight / 2f - draggedInsertIndex = findDraggedInsertIndex( - schedule = schedule, - bounds = scheduleItemBounds, - draggedCenter = draggedCenter, - ) - }, - ) - } - ) { - ScheduleItemCard( - modifier = Modifier.fillMaxWidth(), - wallpaper = wallpaper, - scheduleEntry = entry, - isCurrent = currentWallpaperId == entry.wallpaperId, - isDragged = isDragged, - isDragPlaceholder = isDragged || isSettling, - dragOffsetY = 0f, - onEdit = { - editTargetId.value = entry.wallpaperId - delayInput = entry.delayMs.toString() - }, - onDelete = { - schedule.removeAll { it.wallpaperId == entry.wallpaperId } - persistSchedule() - }, - ) + }, + onDrag = { change, dragAmount -> + change.consume() + val draggingId = + draggedId + ?: return@detectDragGesturesAfterLongPress + val currentHeight = + scheduleItemBounds[draggingId]?.height + ?: draggedItemHeight + draggedOffsetY += dragAmount.y + val draggedCenter = + draggedStartTop + draggedOffsetY + currentHeight / 2f + draggedInsertIndex = + findDraggedInsertIndex( + schedule = schedule, + bounds = scheduleItemBounds, + draggedCenter = draggedCenter, + ) + }, + ) + } + ) { + ScheduleItemCard( + modifier = Modifier.fillMaxWidth(), + wallpaper = wallpaper, + scheduleEntry = entry, + isCurrent = currentWallpaperId == entry.wallpaperId, + isDragged = isDragged, + isDragPlaceholder = isDragged || isSettling, + dragOffsetY = 0f, + onEdit = { + editTargetId.value = entry.wallpaperId + delayInput = entry.delayMs.toString() + }, + onDelete = { + schedule.removeAll { it.wallpaperId == entry.wallpaperId } + persistSchedule() + }, + ) + } } } - } - if (loading) { - item { - Card(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - InfiniteProgressIndicator() - Spacer(Modifier.width(10.dp)) - Text(text = stringResource(R.string.rear_wallpaper_loading)) + if (loading) { + item { + Card(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + InfiniteProgressIndicator() + Spacer(Modifier.width(10.dp)) + Text(text = stringResource(R.string.rear_wallpaper_loading)) + } } } } - } - if (!loading && wallpapers.isEmpty()) { - item { - Card(modifier = Modifier.fillMaxWidth()) { - SuperCard( - title = stringResource(R.string.rear_wallpaper_catalog_empty) - ) + if (!loading && wallpapers.isEmpty()) { + item { + Card(modifier = Modifier.fillMaxWidth()) { + SuperCard( + title = stringResource(R.string.rear_wallpaper_catalog_empty) + ) + } } } } - } - draggedEntry?.let { entry -> - val draggedOverlayStyleState = - remember(entry.wallpaperId) { MutableStyleState(null) } - val draggedOverlayStyle = - remember(draggedStartTop, contentTopInRoot, draggedOffsetY) { - Style { - translationY(draggedStartTop - contentTopInRoot + draggedOffsetY) + draggedEntry?.let { entry -> + val draggedOverlayStyleState = + remember(entry.wallpaperId) { MutableStyleState(null) } + val draggedOverlayStyle = + remember(draggedStartTop, contentTopInRoot, draggedOffsetY) { + Style { + translationY(draggedStartTop - contentTopInRoot + draggedOffsetY) + } } - } - ScheduleItemCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp) - .styleable(draggedOverlayStyleState, draggedOverlayStyle) - .zIndex(3f), - wallpaper = wallpaperMap[entry.wallpaperId], - scheduleEntry = entry, - isCurrent = currentWallpaperId == entry.wallpaperId, - isDragged = true, - isDragPlaceholder = false, - dragOffsetY = 0f, - externalShadow = 18.dp, - onEdit = { - editTargetId.value = entry.wallpaperId - delayInput = entry.delayMs.toString() - }, - onDelete = { - schedule.removeAll { it.wallpaperId == entry.wallpaperId } - draggedId = null - draggedInsertIndex = null - draggedItemHeight = 0f - draggedOffsetY = 0f - persistSchedule() - }, - ) - } ?: settlingOverlay?.let { overlay -> - SettlingScheduleItemOverlay( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp) - .zIndex(3f), - wallpaper = wallpaperMap[overlay.entry.wallpaperId], - scheduleEntry = overlay.entry, - isCurrent = overlay.isCurrent, - startTranslationY = overlay.startTranslationY, - targetTranslationY = overlay.targetTranslationY, - onEdit = { - editTargetId.value = overlay.entry.wallpaperId - delayInput = overlay.entry.delayMs.toString() - }, - onDelete = { - schedule.removeAll { it.wallpaperId == overlay.entry.wallpaperId } - settlingOverlay = null - settlingWallpaperId = null - persistSchedule() - }, - ) + ScheduleItemCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .styleable(draggedOverlayStyleState, draggedOverlayStyle) + .zIndex(3f), + wallpaper = wallpaperMap[entry.wallpaperId], + scheduleEntry = entry, + isCurrent = currentWallpaperId == entry.wallpaperId, + isDragged = true, + isDragPlaceholder = false, + dragOffsetY = 0f, + externalShadow = 18.dp, + onEdit = { + editTargetId.value = entry.wallpaperId + delayInput = entry.delayMs.toString() + }, + onDelete = { + schedule.removeAll { it.wallpaperId == entry.wallpaperId } + draggedId = null + draggedInsertIndex = null + draggedItemHeight = 0f + draggedOffsetY = 0f + persistSchedule() + }, + ) + } ?: settlingOverlay?.let { overlay -> + SettlingScheduleItemOverlay( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .zIndex(3f), + wallpaper = wallpaperMap[overlay.entry.wallpaperId], + scheduleEntry = overlay.entry, + isCurrent = overlay.isCurrent, + startTranslationY = overlay.startTranslationY, + targetTranslationY = overlay.targetTranslationY, + onEdit = { + editTargetId.value = overlay.entry.wallpaperId + delayInput = overlay.entry.delayMs.toString() + }, + onDelete = { + schedule.removeAll { it.wallpaperId == overlay.entry.wallpaperId } + settlingOverlay = null + settlingWallpaperId = null + persistSchedule() + }, + ) + } } } } } } - } - OverlayBottomSheet( - show = pickerMode.value != null, - title = stringResource( - when (pickerMode.value) { - WallpaperPickerMode.ADD_TO_SCHEDULE -> R.string.rear_wallpaper_picker_add - null -> R.string.rear_wallpaper_manager - } - ), - onDismissRequest = { pickerMode.value = null }, - ) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 560.dp), - verticalArrangement = Arrangement.spacedBy(10.dp), - contentPadding = PaddingValues(top = 4.dp, bottom = 34.dp), + OverlayBottomSheet( + show = pickerMode.value != null, + title = stringResource( + when (pickerMode.value) { + WallpaperPickerMode.ADD_TO_SCHEDULE -> R.string.rear_wallpaper_picker_add + null -> R.string.rear_wallpaper_manager + } + ), + onDismissRequest = { pickerMode.value = null }, ) { - if (loading) { - item { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - InfiniteProgressIndicator() - Spacer(Modifier.width(10.dp)) - Text(text = stringResource(R.string.rear_wallpaper_loading)) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 560.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(top = 4.dp, bottom = 34.dp), + ) { + if (loading) { + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + InfiniteProgressIndicator() + Spacer(Modifier.width(10.dp)) + Text(text = stringResource(R.string.rear_wallpaper_loading)) + } } } - } - items(wallpapers, key = { it.wallpaperId }) { wallpaper -> - WallpaperPickerCard( - wallpaper = wallpaper, - isCurrent = wallpaper.wallpaperId == currentWallpaperId, - inSchedule = schedule.any { it.wallpaperId == wallpaper.wallpaperId }, - onAddToSchedule = { addToSchedule(wallpaper.wallpaperId) }, - ) + items(wallpapers, key = { it.wallpaperId }) { wallpaper -> + WallpaperPickerCard( + wallpaper = wallpaper, + isCurrent = wallpaper.wallpaperId == currentWallpaperId, + inSchedule = schedule.any { it.wallpaperId == wallpaper.wallpaperId }, + onAddToSchedule = { addToSchedule(wallpaper.wallpaperId) }, + ) + } } } - } - OverlayDialog( - show = editTargetId.value != null, - title = stringResource(R.string.rear_wallpaper_edit_interval), - onDismissRequest = { editTargetId.value = null }, - ) { - DialogFormColumn { - TextField( - value = delayInput, - onValueChange = { delayInput = it.filter(Char::isDigit) }, - modifier = Modifier.fillMaxWidth(), - label = stringResource(R.string.rear_wallpaper_interval_millis), - singleLine = true, - ) - Button( - onClick = { - val targetId = editTargetId.value ?: return@Button - val delayMs = delayInput.toLongOrNull() - if (delayMs == null || delayMs < RearWallpaperScheduleCodec.MIN_DELAY_MS) { - toast(R.string.rear_wallpaper_interval_invalid) - return@Button - } - val index = schedule.indexOfFirst { it.wallpaperId == targetId } - if (index >= 0) { - schedule[index] = schedule[index].copy(delayMs = delayMs) - persistSchedule() - } - editTargetId.value = null - }, - colors = ButtonDefaults.buttonColorsPrimary(), - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.rear_widget_confirm)) - } - Button( - onClick = { editTargetId.value = null }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.rear_widget_cancel)) + OverlayDialog( + show = editTargetId.value != null, + title = stringResource(R.string.rear_wallpaper_edit_interval), + onDismissRequest = { editTargetId.value = null }, + ) { + DialogFormColumn { + TextField( + value = delayInput, + onValueChange = { delayInput = it.filter(Char::isDigit) }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.rear_wallpaper_interval_millis), + singleLine = true, + ) + Button( + onClick = { + val targetId = editTargetId.value ?: return@Button + val delayMs = delayInput.toLongOrNull() + if (delayMs == null || delayMs < RearWallpaperScheduleCodec.MIN_DELAY_MS) { + toast(R.string.rear_wallpaper_interval_invalid) + return@Button + } + val index = schedule.indexOfFirst { it.wallpaperId == targetId } + if (index >= 0) { + schedule[index] = schedule[index].copy(delayMs = delayMs) + persistSchedule() + } + editTargetId.value = null + }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_confirm)) + } + Button( + onClick = { editTargetId.value = null }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_cancel)) + } } } - } } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt index 7b7a9d3..eb1e16f 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt @@ -84,12 +84,11 @@ fun RearNavigationBar( ) } - if (navigationBarMode == ModuleNavigationBarMode.NORMAL) { - val enable = rememberBlurBackdrop(true) - BlurredBar(backdrop = enable){ + if (navigationBarMode == ModuleNavigationBarMode.NORMAL || navigationBarMode == ModuleNavigationBarMode.SEMI_TRANSPARENT) { + val bar: @Composable () -> Unit = { NavigationBar( modifier = modifier, - color = Color.Transparent, + color = if (navigationBarMode == ModuleNavigationBarMode.NORMAL) MiuixTheme.colorScheme.surface else Color.Transparent, mode = NavigationBarDisplayMode.IconAndText, ) { items.forEach { item -> @@ -102,6 +101,12 @@ fun RearNavigationBar( } } } + if (navigationBarMode == ModuleNavigationBarMode.SEMI_TRANSPARENT) { + val enable = rememberBlurBackdrop(true) + BlurredBar(backdrop = enable, bar) + } else { + bar() + } return } diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt index b4c9db3..eae086d 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt @@ -101,6 +101,11 @@ enum class ModuleNavigationBarMode( FLOATING_GLASS( value = 2, titleRes = R.string.module_navigation_bar_mode_floating_glass, + ), + + SEMI_TRANSPARENT( + value = 3, + titleRes = R.string.module_navigation_bar_mode_semi_transparent, ); companion object { @@ -109,6 +114,7 @@ enum class ModuleNavigationBarMode( NORMAL, FLOATING, FLOATING_GLASS, + SEMI_TRANSPARENT, ) fun fromValue(value: Int): ModuleNavigationBarMode { diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index b37c76d..9e232f2 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -27,6 +27,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -129,7 +131,15 @@ import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic import java.util.concurrent.ConcurrentHashMap +private val AboutPageHorizontalPadding = 12.dp +private val AboutDeviceInfoCardTopPadding = 20.dp +private val AboutDeviceInfoCardBottomPadding = 12.dp +private val AboutDeviceInfoRowVerticalPadding = 8.dp +private val AboutDeviceInfoHeaderBottomSpacing = 8.dp +private val AboutCardSpacing = 8.dp + private val contributorAvatarHttpClient = OkHttpClient() + private object ContributorAvatarCache { private val cache = ConcurrentHashMap() @@ -439,16 +449,20 @@ private fun AboutRootContent( paddingValues, paddingValues, false, - extraStart = WindowInsets.displayCutout.asPaddingValues().calculateLeftPadding(LayoutDirection.Ltr), - extraEnd = WindowInsets.displayCutout.asPaddingValues().calculateRightPadding(LayoutDirection.Ltr), + extraStart = WindowInsets.displayCutout.asPaddingValues() + .calculateLeftPadding(LayoutDirection.Ltr), + extraEnd = WindowInsets.displayCutout.asPaddingValues() + .calculateRightPadding(LayoutDirection.Ltr), ) val logoPadding = pageContentPadding( paddingValues, paddingValues, false, extraTop = 10.dp, - extraStart = WindowInsets.displayCutout.asPaddingValues().calculateLeftPadding(LayoutDirection.Ltr), - extraEnd = WindowInsets.displayCutout.asPaddingValues().calculateRightPadding(LayoutDirection.Ltr), + extraStart = WindowInsets.displayCutout.asPaddingValues() + .calculateLeftPadding(LayoutDirection.Ltr), + extraEnd = WindowInsets.displayCutout.asPaddingValues() + .calculateRightPadding(LayoutDirection.Ltr), ) val density = LocalDensity.current @@ -484,12 +498,19 @@ private fun AboutRootContent( val stage3TotalLength = projectNameY - iconY val versionCodeDelay = stage1TotalLength * 0.5f - versionCodeProgress = ((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay).coerceAtLeast(1f)) - .coerceIn(0f, 1f) - projectNameProgress = ((offset.toFloat() - stage1TotalLength) / stage2TotalLength.coerceAtLeast(1f)) - .coerceIn(0f, 1f) - iconProgress = ((offset.toFloat() - stage1TotalLength - stage2TotalLength) / stage3TotalLength.coerceAtLeast(1f)) - .coerceIn(0f, 1f) + versionCodeProgress = + ((offset.toFloat() - versionCodeDelay) / (stage1TotalLength - versionCodeDelay).coerceAtLeast( + 1f + )) + .coerceIn(0f, 1f) + projectNameProgress = + ((offset.toFloat() - stage1TotalLength) / stage2TotalLength.coerceAtLeast(1f)) + .coerceIn(0f, 1f) + iconProgress = + ((offset.toFloat() - stage1TotalLength - stage2TotalLength) / stage3TotalLength.coerceAtLeast( + 1f + )) + .coerceIn(0f, 1f) } .collect { } } @@ -612,7 +633,7 @@ private fun AboutRootContent( .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .rearAcrylicSource(hazeState) - .padding(horizontal = 12.dp), + .padding(horizontal = AboutPageHorizontalPadding), contentPadding = PaddingValues( top = scrollPadding.calculateTopPadding(), bottom = paddingValues.calculateBottomPadding() + bottomInnerPadding, @@ -644,16 +665,29 @@ private fun AboutRootContent( modifier = Modifier .fillMaxWidth() .padding(bottom = scrollPadding.calculateBottomPadding()), + verticalArrangement = Arrangement.spacedBy(AboutCardSpacing), ) { ArtStaggeredReveal( visible = true, - revealKey = "contributors", + revealKey = "device_info", delayMillis = 36, ) { + val layoutDirection = LocalLayoutDirection.current + val deviceInfoCardPadding = PaddingValues( + start = BasicComponentDefaults.InsideMargin.calculateStartPadding( + layoutDirection + ), + top = AboutDeviceInfoCardTopPadding, + end = BasicComponentDefaults.InsideMargin.calculateEndPadding( + layoutDirection + ), + bottom = AboutDeviceInfoCardBottomPadding, + ) + val deviceInfoRowPadding = + PaddingValues(vertical = AboutDeviceInfoRowVerticalPadding) + Card( modifier = Modifier - .padding(horizontal = 8.dp) - .padding(bottom = 8.dp) .textureBlur( backdrop = backdrop, shape = SmoothRoundedCornerShape(16.dp), @@ -672,37 +706,40 @@ private fun AboutRootContent( Color.Transparent, ), ) { - BasicComponent( - enabled = true, + Column( + modifier = Modifier.padding(deviceInfoCardPadding), ) { Text( text = DeviceConfigTools.deviceName, + modifier = Modifier.padding(bottom = AboutDeviceInfoHeaderBottomSpacing), fontSize = 24.sp, fontWeight = FontWeight.Medium, color = BasicComponentDefaults.titleColor().color, ) - } - - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.device_name), - summary = DeviceConfigTools.marketName, - ) - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.android_version), - summary = DeviceConfigTools.androidVersion, - ) - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.os_version), - summary = OSVersionTools.addVersionSuffix(context), - ) - - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.subsceen_version), - summary = DeviceConfigTools.getSubSceenVersion(context), - ) + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.device_name), + summary = DeviceConfigTools.marketName, + insideMargin = deviceInfoRowPadding, + ) + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.android_version), + summary = DeviceConfigTools.androidVersion, + insideMargin = deviceInfoRowPadding, + ) + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.os_version), + summary = OSVersionTools.addVersionSuffix(context), + insideMargin = deviceInfoRowPadding, + ) + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.subsceen_version), + summary = DeviceConfigTools.getSubSceenVersion(context), + insideMargin = deviceInfoRowPadding, + ) + } } } @@ -711,53 +748,11 @@ private fun AboutRootContent( revealKey = "contributors", delayMillis = 36, ) { - Card( - modifier = Modifier - .padding(horizontal = 8.dp) - .textureBlur( - backdrop = backdrop, - shape = SmoothRoundedCornerShape(16.dp), - blurRadius = 60f, - noiseCoefficient = 0.001f, - colors = BlurColors( - blendColors = visualTokens.cardBlendColors, - brightness = 0f, - contrast = 1f, - saturation = 1f, - ), - enabled = true, - ), - colors = CardDefaults.defaultColors( - Color.Transparent, - Color.Transparent, - ), - ) { - SuperCard( - title = stringResource(R.string.credits_contributors_title), - summary = stringResource(R.string.credits_contributors_desc), - onClick = onOpenContributors, - endActions = { - Icon( - imageVector = MiuixIcons.Create, - tint = colorScheme.onSurface, - contentDescription = null, - ) - }, - ) - } - } - - - entries.forEachIndexed { index, entry -> - ArtStaggeredReveal( - visible = true, - revealKey = entry.url, - delayMillis = (54 + index * 18).coerceAtMost(150), + Column( + verticalArrangement = Arrangement.spacedBy(AboutCardSpacing), ) { Card( modifier = Modifier - .padding(top = 8.dp) - .padding(horizontal = 8.dp) .textureBlur( backdrop = backdrop, shape = SmoothRoundedCornerShape(16.dp), @@ -777,20 +772,67 @@ private fun AboutRootContent( ), ) { SuperCard( - title = stringResource(entry.titleRes), - summary = stringResource(entry.summaryRes), - onClick = { - context.startActivity(Intent(Intent.ACTION_VIEW, entry.url.toUri())) - }, + title = stringResource(R.string.credits_contributors_title), + summary = stringResource(R.string.credits_contributors_desc), + onClick = onOpenContributors, endActions = { Icon( - imageVector = MiuixIcons.Link, + imageVector = MiuixIcons.Create, tint = colorScheme.onSurface, contentDescription = null ) } ) } + + entries.forEachIndexed { index, entry -> + ArtStaggeredReveal( + visible = true, + revealKey = entry.url, + delayMillis = (54 + index * 18).coerceAtMost(150), + ) { + Card( + modifier = Modifier + .textureBlur( + backdrop = backdrop, + shape = SmoothRoundedCornerShape(16.dp), + blurRadius = 60f, + noiseCoefficient = 0.001f, + colors = BlurColors( + blendColors = visualTokens.cardBlendColors, + brightness = 0f, + contrast = 1f, + saturation = 1f, + ), + enabled = true, + ), + colors = CardDefaults.defaultColors( + Color.Transparent, + Color.Transparent, + ), + ) { + SuperCard( + title = stringResource(entry.titleRes), + summary = stringResource(entry.summaryRes), + onClick = { + context.startActivity( + Intent( + Intent.ACTION_VIEW, + entry.url.toUri() + ) + ) + }, + endActions = { + Icon( + imageVector = MiuixIcons.Link, + tint = colorScheme.onSurface, + contentDescription = null + ) + } + ) + } + } + } } } } @@ -817,12 +859,12 @@ private fun ContributorListContent( .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .rearAcrylicSource(hazeState) - .padding(horizontal = 12.dp), + .padding(horizontal = AboutPageHorizontalPadding), contentPadding = PaddingValues( top = paddingValues.calculateTopPadding(), bottom = paddingValues.calculateBottomPadding() + bottomInnerPadding, ), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(AboutCardSpacing), overscrollEffect = null, ) { item { diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt index 0154a93..1372935 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt @@ -21,11 +21,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,6 +41,7 @@ import androidx.compose.ui.graphics.lerp import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp @@ -520,7 +523,13 @@ private fun ConfigNodeList( resolveFavoriteNodeId: (ConfigNode) -> String? = { null }, onToggleFavorite: (ConfigNode) -> Unit = {}, ) { + val listState = rememberLazyListState() + val isListScrolling by remember { + derivedStateOf { listState.isScrollInProgress } + } + LazyColumn( + state = listState, modifier = Modifier .fillMaxHeight() .scrollEndHaptic() @@ -596,6 +605,7 @@ private fun ConfigNodeList( ConfigNodeRowWithFavoriteMenu( node = child, prefsManager = prefsManager, + isListScrolling = isListScrolling, onOpenCategory = onOpenCategory, onOpenAppList = onOpenAppList, onOpenManager = onOpenManager, @@ -615,6 +625,7 @@ private fun ConfigNodeList( ConfigNodeRowWithFavoriteMenu( node = node, prefsManager = prefsManager, + isListScrolling = isListScrolling, onOpenCategory = onOpenCategory, onOpenAppList = onOpenAppList, onOpenManager = onOpenManager, @@ -633,6 +644,7 @@ private fun ConfigNodeList( private fun ConfigNodeRowWithFavoriteMenu( node: ConfigNode, prefsManager: PrefsManager, + isListScrolling: Boolean, onOpenCategory: (ConfigCategory) -> Unit, onOpenAppList: (ConfigItem) -> Unit, onOpenManager: (ConfigItem) -> Unit, @@ -650,6 +662,7 @@ private fun ConfigNodeRowWithFavoriteMenu( Box( modifier = Modifier.configNodeLongPress( enabled = canFavorite, + isScrolling = isListScrolling, onLongPress = { showFavoritePopup = true }, ) ) { @@ -745,16 +758,18 @@ private fun FavoritePopupIcon( private fun Modifier.configNodeLongPress( enabled: Boolean, + isScrolling: Boolean, onLongPress: () -> Unit, ): Modifier { - if (!enabled) return this + if (!enabled || isScrolling) return this - return this.pointerInput(onLongPress) { + return this.pointerInput(onLongPress, isScrolling) { awaitEachGesture { val down = awaitFirstDown( requireUnconsumed = false, pass = PointerEventPass.Initial, ) + if (isScrolling) return@awaitEachGesture val longPressTriggered = withTimeoutOrNull( timeMillis = viewConfiguration.longPressTimeoutMillis, ) { @@ -762,7 +777,7 @@ private fun Modifier.configNodeLongPress( val event = awaitPointerEvent(PointerEventPass.Initial) val change = event.changes.firstOrNull { it.id == down.id } ?: return@withTimeoutOrNull false - if (!change.pressed || change.isConsumed) { + if (!change.pressed || change.isConsumed || change.positionChanged() || isScrolling) { return@withTimeoutOrNull false } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt index 811723e..51fc7f7 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt @@ -889,7 +889,7 @@ private fun UpdateInfoCard(currentHash: String, latestHash: String?, checking: B } @Composable -fun InfoLine(title: String, value: String,modifier: Modifier = Modifier) { +fun InfoLine(title: String, value: String, modifier: Modifier = Modifier) { Column(modifier = modifier) { Text(text = title, style = MiuixTheme.textStyles.headline1) Spacer(modifier = Modifier.height(2.dp)) diff --git a/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt b/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt index a310a29..3c32e0c 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/PageUtils.kt @@ -1,44 +1,22 @@ -// Copyright 2025, compose-miuix-ui contributors -// SPDX-License-Identifier: Apache-2.0 - package hk.uwu.reareye.utils import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.navigationBars import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import top.yukonga.miuix.kmp.basic.ScrollBehavior -import top.yukonga.miuix.kmp.basic.SmallTopAppBar -import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.blur.BlendColorEntry import top.yukonga.miuix.kmp.blur.BlurColors import top.yukonga.miuix.kmp.blur.LayerBackdrop import top.yukonga.miuix.kmp.blur.textureBlur import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.overScrollVertical -import top.yukonga.miuix.kmp.utils.scrollEndHaptic - -fun Modifier.pageScrollModifiers( - enableScrollEndHaptic: Boolean, - showTopAppBar: Boolean, - topAppBarScrollBehavior: ScrollBehavior, -): Modifier = this - .then(if (enableScrollEndHaptic) Modifier.scrollEndHaptic() else Modifier) - .overScrollVertical() - .then(if (showTopAppBar) Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) else Modifier) - .fillMaxHeight() @Composable fun pageContentPadding( @@ -51,7 +29,8 @@ fun pageContentPadding( ): PaddingValues { val topPadding = innerPadding.calculateTopPadding() + extraTop val bottomPadding = if (isWideScreen) { - WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + outerPadding.calculateBottomPadding() + WindowInsets.navigationBars.asPaddingValues() + .calculateBottomPadding() + outerPadding.calculateBottomPadding() } else { outerPadding.calculateBottomPadding() } @@ -65,44 +44,6 @@ fun pageContentPadding( } } -@Composable -fun AdaptiveTopAppBar( - title: String, - showTopAppBar: Boolean, - isWideScreen: Boolean, - scrollBehavior: ScrollBehavior, - subtitle: String = "", - color: Color = MiuixTheme.colorScheme.surface, - navigationIcon: @Composable () -> Unit = {}, - actions: @Composable RowScope.() -> Unit = {}, - bottomContent: @Composable () -> Unit = {}, -) { - if (showTopAppBar) { - if (isWideScreen) { - SmallTopAppBar( - title = title, - subtitle = subtitle, - color = color, - scrollBehavior = scrollBehavior, - defaultWindowInsetsPadding = false, - navigationIcon = navigationIcon, - actions = actions, - bottomContent = bottomContent, - ) - } else { - TopAppBar( - title = title, - subtitle = subtitle, - color = color, - scrollBehavior = scrollBehavior, - navigationIcon = navigationIcon, - actions = actions, - bottomContent = bottomContent, - ) - } - } -} - @Composable fun BlurredBar( backdrop: LayerBackdrop?, diff --git a/app/src/main/java/hk/uwu/reareye/utils/blend/BlendTokenConfig.kt b/app/src/main/java/hk/uwu/reareye/utils/blend/BlendTokenConfig.kt deleted file mode 100644 index 01ccf96..0000000 --- a/app/src/main/java/hk/uwu/reareye/utils/blend/BlendTokenConfig.kt +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2026, compose-miuix-ui contributors -// SPDX-License-Identifier: Apache-2.0 - -package hk.uwu.reareye.utils.blend - -import androidx.compose.ui.graphics.Color -import top.yukonga.miuix.kmp.blur.BlendColorEntry -import top.yukonga.miuix.kmp.blur.BlurBlendMode - -object BlendTokenConfig { - - /** - * 定义深色主题下的各种强度的混合颜色配置。 - */ - object DarkColors { - val DEFAULT = listOf( - BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.Overlay), - BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.HardLight), - ) - - val EXTRA_HEAVY = listOf( - BlendColorEntry(Color(0xE6A1A1A1), BlurBlendMode.ColorBurn), - BlendColorEntry(Color(0x4DE6E6E6), BlurBlendMode.SrcOver), - BlendColorEntry(Color(0x11B0B0B0), BlurBlendMode.SrcOver), // 假设 0xB0B0B0 是对应的颜色 - ) - - val HEAVY = listOf( - BlendColorEntry(Color(0x80121212), BlurBlendMode.ColorBurn), // 假设 0x121212 是对应的颜色 - BlendColorEntry(Color(0x40555555), BlurBlendMode.SrcOver), // 假设 0x555555 是对应的颜色 - ) - - val LIGHT = listOf( - BlendColorEntry(Color(0x61616161), BlurBlendMode.ColorBurn), // 假设 0x61616161 是对应的颜色 - BlendColorEntry(Color(0x4D4D4D4D), BlurBlendMode.SrcOver), // 假设 0x4D4D4D4D 是对应的颜色 - ) - - val EXTRA_LIGHT = listOf( - BlendColorEntry(Color(0x4D4D4D4D), BlurBlendMode.ColorBurn), // 假设 0x4D4D4D4D 是对应的颜色 - BlendColorEntry(Color(0x33333333), BlurBlendMode.SrcOver), // 假设 0x33333333 是对应的颜色 - ) - } - - /** - * 定义浅色主题下的各种强度的混合颜色配置。 - */ - object LightColors { - val DEFAULT = listOf( - BlendColorEntry(Color(0x701A1A1A), BlurBlendMode.ColorDodge), // 假设 0x1A1A1A 是对应的颜色 - BlendColorEntry(Color(0x5D5D5D5D), BlurBlendMode.SrcOver), // 假设 0x5D5D5D5D 是对应的颜色 - ) - - val EXTRA_HEAVY = listOf( - BlendColorEntry(Color(0x701A1A1A), BlurBlendMode.ColorDodge), // 假设 0x1A1A1A 是对应的颜色 - BlendColorEntry(Color(0x60606060), BlurBlendMode.SrcOver), // 假设 0x60606060 是对应的颜色 - ) - - val HEAVY = listOf( - BlendColorEntry(Color(0x5A5A5A5A), BlurBlendMode.ColorDodge), // 假设 0x5A5A5A5A 是对应的颜色 - BlendColorEntry(Color(0x37373737), BlurBlendMode.SrcOver), // 假设 0x37373737 是对应的颜色 - ) - - val LIGHT = listOf( - BlendColorEntry(Color(0x7A7A7A7A), BlurBlendMode.ColorDodge), // 假设 0x7A7A7A7A 是对应的颜色 - BlendColorEntry(Color(0x40404040), BlurBlendMode.SrcOver), // 假设 0x40404040 是对应的颜色 - ) - - val EXTRA_LIGHT = listOf( - BlendColorEntry(Color(0x80808080), BlurBlendMode.ColorDodge), // 假设 0x80808080 是对应的颜色 - BlendColorEntry(Color(0x27272727), BlurBlendMode.SrcOver), // 假设 0x27272727 是对应的颜色 - ) - } - - /** - * 定义深色主题下的混合模式。 - * 注意:这里的 BlendMode 可能也需要根据 Miuix KMP 的 API 进行调整。 - * 例如,如果 Miuix 使用的是枚举而非 Int,这里应相应改变。 - */ - object DarkModes { - val DEFAULT = listOf(BlurBlendMode.ColorDodge, BlurBlendMode.LinearLight, BlurBlendMode.LinearLight) - } - - object LightModes { - val DEFAULT = listOf(BlurBlendMode.ColorBurn, BlurBlendMode.LinearLight) - } - - /** - * 定义效果强度。 - */ - object Effects { - const val DEFAULT = 66 - const val EXTRA_THIN = 30 - const val HEAVY = 74 - const val THIN = 52 - } -} diff --git a/app/src/main/java/hk/uwu/reareye/utils/effect/FrameTimeSeconds.kt b/app/src/main/java/hk/uwu/reareye/utils/effect/FrameTimeSeconds.kt index 2040e37..f7991c2 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/effect/FrameTimeSeconds.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/effect/FrameTimeSeconds.kt @@ -31,7 +31,7 @@ fun rememberFrameTimeSeconds( time = startOffset + - (now - start) / 1_000_000_000f + (now - start) / 1_000_000_000f } } diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt index c65682e..9e7f1d2 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt @@ -9,12 +9,16 @@ import hk.uwu.reareye.utils.RootHelper.hasRootAccess object DeviceConfigTools { // 我就说我设备名字怎么就对不上了,这玩意还要 Root 获取,破烂 - val getdevice = if (hasRootAccess()) executeRootCommand("getprop persist.private.device_name") else Pair(20, "无法获取Root来获取设备名字") - val deviceName = if (getdevice.first == 0) getdevice.second else "无法获取Root来获取设备名字" + val getdevice = + if (hasRootAccess()) executeRootCommand("getprop persist.private.device_name") else Pair( + 20, + "无法获取Root来获取设备名字" + ) + val deviceName = if (getdevice.first == 0) getdevice.second else "无法获取Root来获取设备名字" val androidVersion: String = getSystemProperties("ro.build.version.release") - + val marketName by lazy { val marketName: String = getSystemProperties("ro.product.marketname") @@ -23,7 +27,7 @@ object DeviceConfigTools { } - fun bigtextone(st:String): String { + fun bigtextone(st: String): String { val formattedBrand = st.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } @@ -34,10 +38,11 @@ object DeviceConfigTools { fun getSystemProperties(key: String): String { val ret: String = try { - Class.forName("android.os.SystemProperties").getDeclaredMethod("get", String::class.java).invoke(null, key) as String + Class.forName("android.os.SystemProperties") + .getDeclaredMethod("get", String::class.java).invoke(null, key) as String } catch (iAE: IllegalArgumentException) { throw iAE - } catch (e: Exception) { + } catch (_: Exception) { "" } return ret diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1638f75..df40a42 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -475,4 +475,5 @@ OS 版本 Android 版本 背屏版本 + 半透明 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3fd4ebf..474e117 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -475,4 +475,5 @@ Widget: %2$s · Priority: %3$d OS Version Android Version SubSceen Version + Semi-transparent From 36f540de45fca9d2fa719c6f77c8d537971d19b5 Mon Sep 17 00:00:00 2001 From: ghhccghk <2137610394@qq.com> Date: Mon, 4 May 2026 22:16:46 +0800 Subject: [PATCH 09/27] feat: add open-source licenses screen and improve sub-screen version retrieval - **feat**: Integrated `AboutLibraries` plugin and implemented `AboutLibrariesTools` to parse and display library information using `kotlinx-serialization`. - **feat**: Added a new "Licenses" navigation route and dedicated `LicenseContent` UI to the About screen. - **feat**: Added localized string resources for the new licenses section. - **fix**: Added error handling to `getSubSceenVersion` in `DeviceConfigTools` to prevent crashes if the target package is missing. - **build**: Added `kotlinx-serialization` plugin and dependency, and configured the `aboutLibraries` plugin in `build.gradle.kts`. - **proguard**: Added keep rules for `AboutLibrariesToolsKt` to prevent issues with reflection or serialization in minified builds. Signed-off-by: ghhccghk <2137610394@qq.com> --- app/build.gradle.kts | 38 ++++ app/proguard-rules.pro | 1 + .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 54 +++++- .../utils/other/AboutLibrariesTools.kt | 173 ++++++++++++++++++ .../reareye/utils/other/DeviceConfigTools.kt | 12 +- app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + build.gradle.kts | 1 + gradle/libs.versions.toml | 4 + 9 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c2c235e..f1b9c13 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.ksp) alias(libs.plugins.compose.compiler) + alias(libs.plugins.serialization) + id("com.mikepenz.aboutlibraries.plugin") } val gitCommitCount: Int by lazy { runGitCommand("rev-list", "--count", "HEAD")?.toIntOrNull() ?: 0 } @@ -120,6 +122,41 @@ androidComponents { } } +aboutLibraries { + offlineMode = false + collect { + configPath.file("config") // TODO(ASAP) libraries json ignored + fetchRemoteLicense.set(false) + } + export { + // Remove the "generated" timestamp to allow for reproducible builds + prettyPrint.set(true) + } + license { + // TODO https://github.com/mikepenz/AboutLibraries/issues/1190 + strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL + allowedLicensesMap.put("Other", listOf("com.github.bumptech.glide:glide")) + allowedLicenses.addAll( + "Apache-2.0", + "LGPL", + "GNU Lesser General Public License v2.1", + "BSD-2-Clause", + "BSD-3-Clause", + "CC0-1.0", + "MIT", + "EPL-1.0", + "GPL-3.0-only", + "GNU Lesser General Public License v3.0" + ) + } + library { + // Enable the duplication mode, allows to merge, or link dependencies which relate + duplicationMode.set(com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE) + // Configure the duplication rule, to match "duplicates" with + duplicationRule.set(com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP) + } +} + tasks.withType().configureEach { compilerOptions { jvmTarget = JvmTarget.JVM_17 @@ -173,4 +210,5 @@ dependencies { implementation(libs.lyricon.central) implementation(libs.lyricon.subscriber) implementation(libs.superlyric) + implementation(libs.kotlinx.serialization.json) } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a9af6e3..a3b5b75 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -39,5 +39,6 @@ # --- Tool --- -keep class hk.uwu.reareye.hook.** { *; } +-keep class hk.uwu.reareye.utils.other.AboutLibrariesToolsKt -keep class com.hchen.superlyricapi.* {*;} -dontwarn android.os.ServiceManager \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 9e232f2..348eed6 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape @@ -94,7 +95,9 @@ import hk.uwu.reareye.ui.theme.rememberAcrylicHazeStyle import hk.uwu.reareye.utils.blend.ColorBlendToken import hk.uwu.reareye.utils.effect.BgEffectBackground import hk.uwu.reareye.utils.other.DeviceConfigTools +import hk.uwu.reareye.utils.other.LibraryItem import hk.uwu.reareye.utils.other.OSVersionTools +import hk.uwu.reareye.utils.other.loadLibraries import hk.uwu.reareye.utils.pageContentPadding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.onEach @@ -180,6 +183,7 @@ private object ContributorAvatarCache { private sealed interface AboutRoute { data object Root : AboutRoute data object Contributors : AboutRoute + data object Licenses : AboutRoute } private data class CreditEntry( @@ -321,7 +325,7 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { ) } - BackHandler(enabled = route is AboutRoute.Contributors) { + BackHandler(enabled = route is AboutRoute.Contributors || route is AboutRoute.Licenses) { route = AboutRoute.Root } @@ -342,7 +346,7 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { TopAppBar( modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), color = Color.Transparent, - title = stringResource(R.string.credits_contributors_title), + title = if (route is AboutRoute.Contributors) { stringResource(R.string.credits_contributors_title) } else { stringResource(R.string.licenses_name)}, navigationIcon = { IconButton(onClick = { route = AboutRoute.Root }) { Icon( @@ -368,7 +372,7 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { targetState = route, contentKey = { it }, transitionSpec = { - val forward = targetState is AboutRoute.Contributors + val forward = targetState is AboutRoute.Contributors || targetState is AboutRoute.Licenses fadeIn( animationSpec = tween( @@ -410,6 +414,7 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { versionText = versionText, entries = entries, onOpenContributors = { route = AboutRoute.Contributors }, + onOpenLibraries = { route = AboutRoute.Licenses }, lazyListState = lazyListState, scrollProgress = scrollProgress, onLogoHeightChanged = { logoHeightPx = it } @@ -422,6 +427,13 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { hazeState = hazeState, state = contributorState, ) + + AboutRoute.Licenses -> LicenseContent( + bottomInnerPadding = bottomInnerPadding, + paddingValues = paddingValues, + scrollBehavior = scrollBehavior, + hazeState = hazeState, + ) } } } @@ -439,6 +451,7 @@ private fun AboutRootContent( scrollProgress: Float, onLogoHeightChanged: (Int) -> Unit, lazyListState: LazyListState, + onOpenLibraries: () -> Unit, ) { val context = LocalContext.current val visualTokens = rememberAboutVisualTokens() @@ -945,6 +958,41 @@ private fun ContributorListContent( } } +@Composable +private fun LicenseContent( + bottomInnerPadding: Dp, + paddingValues: PaddingValues, + scrollBehavior: ScrollBehavior, + hazeState: HazeState, +){ + val context = LocalContext.current + val data = remember { + loadLibraries(context) + } + + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .scrollEndHaptic() + .overScrollVertical() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .rearAcrylicSource(hazeState) + .padding(horizontal = 12.dp), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + bottomInnerPadding, + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + overscrollEffect = null, + ){ + items(data.libraries) { + LibraryItem(it) + } + } + +} + @Composable private fun ContributorCard( item: ContributorProfile, diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt new file mode 100644 index 0000000..24694e4 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt @@ -0,0 +1,173 @@ +package hk.uwu.reareye.utils.other + +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.core.net.toUri +import hk.uwu.reareye.R +import hk.uwu.reareye.ui.components.card.SuperCard +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.ButtonColors +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.extended.Link +import top.yukonga.miuix.kmp.theme.MiuixTheme + +@Serializable +data class AboutLibraries( + val libraries: List, + val licenses: Map +) + +@Serializable +data class Library( + val uniqueId: String, + val artifactVersion: String, + val name: String, + val description: String? = null, + val website: String? = null, + val developers: List = emptyList(), + val organization: Organization? = null, + val licenses: List = emptyList() +) + +@Serializable +data class Developer( + val name: String? = null, + val organisationUrl: String? = null +) + +@Serializable +data class Organization( + val name: String, + val url: String? = null +) + +@Serializable +data class License( + val name: String, + val url: String, + val content: String? = null +) + +private val json = Json { + ignoreUnknownKeys = true +} + +fun loadLibraries(context: Context): AboutLibraries { + val inputStream = context.resources.openRawResource(R.raw.aboutlibraries) + + val jsonString = inputStream + .bufferedReader() + .use { it.readText() } + + return json.decodeFromString(jsonString) +} + +@Composable +fun LibraryItem(lib: Library) { + val context = LocalContext.current + val hasLink = lib.website != null + + Card(modifier = Modifier.fillMaxWidth()) { + SuperCard( + title = lib.name, + summary = buildLibrarySummary(lib), + endActions = { + if (hasLink) { + Button( + colors = ButtonColors( + color = Color.Transparent, + disabledColor = Color.Transparent, + contentColor = Color.Transparent, + disabledContentColor = Color.Transparent, + ), + onClick = { + lib.website?.let { targetLink -> + context.startActivity( + Intent( + Intent.ACTION_VIEW, + targetLink.toUri() + ) + ) + } + } + ) { + Icon( + imageVector = MiuixIcons.Link, + tint = MiuixTheme.colorScheme.onSurface, + contentDescription = null, + ) + } + } + }, + bottomAction = { + if (lib.developers.isNotEmpty()) { + LibraryDevelopersText(lib.developers) + } + if (lib.licenses.isNotEmpty()) { + Text(text = "License: " + lib.licenses.joinToString(", ")) + } + }, + ) + } +} + +private fun buildLibrarySummary(lib: Library): String? { + val parts = buildList { + add(lib.artifactVersion) + lib.organization?.name?.let { add(it) } + lib.description?.takeIf { it.isNotBlank() }?.let { add(it) } + } + return parts.takeIf { it.isNotEmpty() }?.joinToString("\n") +} + +@Composable +private fun LibraryDevelopersText(developers: List) { + val annotated = buildAnnotatedString { + developers.forEachIndexed { index, developer -> + val name = developer.name ?: "Unknown" + val url = developer.organisationUrl + + if (url != null) { + append("Developer: ") + val start = length + append(name) + addLink( + LinkAnnotation.Url( + url = url, + styles = TextLinkStyles( + SpanStyle( + color = MiuixTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + ) + ) + ), + start = start, + end = length, + ) + } else { + append("Developer: ") + append(name) + } + + if (index < developers.size - 1) { + append(", ") + } + } + } + Text(text = annotated) +} \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt index 9e7f1d2..bcb6f24 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt @@ -3,6 +3,7 @@ package hk.uwu.reareye.utils.other import android.annotation.SuppressLint import android.content.Context import android.os.Build +import android.util.Log import hk.uwu.reareye.utils.RootHelper.executeRootCommand import hk.uwu.reareye.utils.RootHelper.hasRootAccess @@ -49,8 +50,13 @@ object DeviceConfigTools { } fun getSubSceenVersion(context: Context): String { - val packageManager = context.packageManager - val packageInfo = packageManager.getPackageInfo("com.xiaomi.subscreencenter", 0) - return packageInfo.versionName.toString() + try { + val packageManager = context.packageManager + val packageInfo = packageManager.getPackageInfo("com.xiaomi.subscreencenter", 0) + return packageInfo.versionName.toString() + } catch (e: Exception) { + Log.e("getSubSceenVersion", e.message,e) + return "无法获取小米分屏版本" + } } } \ 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 df40a42..307dcc6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -476,4 +476,6 @@ Android 版本 背屏版本 半透明 + 引用 + 查看我们引用了哪些库 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 474e117..198cf06 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -476,4 +476,6 @@ Widget: %2$s · Priority: %3$d Android Version SubSceen Version Semi-transparent + Quote + See which libraries we have referenced diff --git a/build.gradle.kts b/build.gradle.kts index 3cd7a9e..45807e4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.ksp) apply false alias(libs.plugins.compose.compiler) apply false + id("com.mikepenz.aboutlibraries.plugin") version "14.1.0" apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 656d847..567a463 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "9.1.1" kotlin = "2.3.20" +kotlinx-serialization-json = "1.11.0" ksp = "2.3.6" rovo89-xposed-api = "82" yukihookapi = "1.3.1" @@ -27,6 +28,7 @@ foundation-layout = "1.10.6" mmkv = "2.4.0" [plugins] +serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } @@ -34,6 +36,7 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = " [libraries] androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } rovo89-xposed-api = { group = "de.robv.android.xposed", name = "api", version.ref = "rovo89-xposed-api" } yukihookapi = { group = "com.highcapable.yukihookapi", name = "api", version.ref = "yukihookapi" } yukihookapi-ksp-xposed = { group = "com.highcapable.yukihookapi", name = "ksp-xposed", version.ref = "yukihookapi" } @@ -69,3 +72,4 @@ gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } dexkit = { group = "org.luckypray", name = "dexkit", version.ref = "dexkit" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundation-layout" } mmkv = { group = "com.tencent", name = "mmkv-static", version.ref = "mmkv" } + From ff2eafaa5ddaba8b670b85f84c1bebd611be96d8 Mon Sep 17 00:00:00 2001 From: ghhccghk <2137610394@qq.com> Date: Mon, 4 May 2026 22:21:58 +0800 Subject: [PATCH 10/27] feat: add licenses entry to About screen - Added a `SuperCard` to the About screen to display and provide access to open-source licenses. - Integrated `onOpenLibraries` click action and used localized string resources for the title and summary. Signed-off-by: ghhccghk <2137610394@qq.com> --- app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 348eed6..dec9f92 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -796,6 +796,11 @@ private fun AboutRootContent( ) } ) + SuperCard( + title = stringResource(R.string.licenses_name), + summary = stringResource(R.string.licenses_name_summary), + onClick = onOpenLibraries, + ) } entries.forEachIndexed { index, entry -> From e7ac291305154cbbdc4a38557d35dfbbfadd35cd Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 4 May 2026 22:53:48 +0800 Subject: [PATCH 11/27] feat: restrict some hook is only load on devices that support rear screen & skip display subscreen version for not supported devices --- .../java/hk/uwu/reareye/hook/scopes/Scope.kt | 11 +++++++ .../subscreencenter/SubscreenCenterScope.kt | 31 ++++++++++++------- .../reareye/hook/scopes/system/SystemScope.kt | 27 ++++++++++------ .../scopes/thememanager/ThemeManagerScope.kt | 21 +++++++++---- .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 14 ++++++--- .../reareye/utils/other/DeviceConfigTools.kt | 18 ++++++----- .../uwu/reareye/utils/other/OSVersionTools.kt | 2 +- 7 files changed, 85 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/Scope.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/Scope.kt index 8e6c1dd..be438cb 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/Scope.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/Scope.kt @@ -1,7 +1,18 @@ package hk.uwu.reareye.hook.scopes +import com.highcapable.kavaref.KavaRef.Companion.resolve +import com.highcapable.kavaref.extension.toClass import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker interface Scope { val hooks: List + val isRearDevice + get() = Companion.isRearDevice + + companion object { + val isRearDevice: Boolean = "android.os.SystemProperties".toClass().resolve().firstMethod { + name = "getInt" + parameters(String::class.java, Int::class.java) + }.invoke("persist.sys.multi_display_type", 1) == 6 + } } \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt index 0b08f99..76ecb12 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/subscreencenter/SubscreenCenterScope.kt @@ -1,6 +1,7 @@ package hk.uwu.reareye.hook.scopes.subscreencenter import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog import hk.uwu.reareye.hook.scopes.Scope import hk.uwu.reareye.hook.scopes.subscreencenter.modules.MusicControlWhitelistModule import hk.uwu.reareye.hook.scopes.subscreencenter.modules.RearWallpaperHook @@ -13,15 +14,23 @@ import hk.uwu.reareye.hook.scopes.subscreencenter.modules.rearwidget.RearWidgetH import hk.uwu.reareye.hook.scopes.subscreencenter.modules.rearwidget.SystemUiNotificationBridgeHook class SubscreenCenterScope : Scope { - override val hooks: List = listOf( - MusicControlWhitelistModule(), - SubScreenBackHomeWhitelistModule(), - VideoLoopModule(), - RearWallpaperHook(), - RearWidgetHook(), - SystemUiNotificationBridgeHook(), - LyriconHook(), - VideoVolumeHook(), - ExtraTimeTipHook() - ) + override val hooks: List = buildList { + if (isRearDevice) { + addAll( + listOf( + MusicControlWhitelistModule(), + SubScreenBackHomeWhitelistModule(), + VideoLoopModule(), + RearWallpaperHook(), + RearWidgetHook(), + SystemUiNotificationBridgeHook(), + LyriconHook(), + VideoVolumeHook(), + ExtraTimeTipHook() + ) + ) + } else { + YLog.debug("This device is not support rear screen, skip load some features that this device is not supported") + } + } } diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt index a63a405..4a5aaea 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt @@ -1,6 +1,7 @@ package hk.uwu.reareye.hook.scopes.system import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog import hk.uwu.reareye.hook.scopes.Scope import hk.uwu.reareye.hook.scopes.system.modules.BackgroundWhitelistModule import hk.uwu.reareye.hook.scopes.system.modules.DisableRearScreenCoverHook @@ -12,13 +13,21 @@ import hk.uwu.reareye.hook.scopes.system.modules.misc.GMSUnlockModule class SystemScope : Scope { - override val hooks: List = listOf( - RearScreenActivityWhitelistModule(), - BackgroundWhitelistModule(), - GMSUnlockModule(), - DisableRearScreenCoverHook(), - DisableSubScreenDoubleTapSleepHook(), - DisableSubScreenDoubleTapWakeHook(), - DisableSubScreenHighLoadModeHook() - ) + override val hooks: List = buildList { + add(GMSUnlockModule()) + if (isRearDevice) { + addAll( + listOf( + RearScreenActivityWhitelistModule(), + BackgroundWhitelistModule(), + DisableRearScreenCoverHook(), + DisableSubScreenDoubleTapSleepHook(), + DisableSubScreenDoubleTapWakeHook(), + DisableSubScreenHighLoadModeHook() + ) + ) + } else { + YLog.debug("This device is not support rear screen, skip load some features that this device is not supported") + } + } } \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/ThemeManagerScope.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/ThemeManagerScope.kt index 37b9eab..2541af6 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/ThemeManagerScope.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/thememanager/ThemeManagerScope.kt @@ -1,6 +1,7 @@ package hk.uwu.reareye.hook.scopes.thememanager import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog import hk.uwu.reareye.hook.scopes.Scope import hk.uwu.reareye.hook.scopes.thememanager.modules.RearWallpaperThemeManagerSyncHook import hk.uwu.reareye.hook.scopes.thememanager.modules.UnlockTemplateMaximumLimitHook @@ -8,10 +9,18 @@ import hk.uwu.reareye.hook.scopes.thememanager.modules.UnlockVideoRestrictionsHo import hk.uwu.reareye.hook.scopes.thememanager.modules.UnmuteVideoWallpaperHook class ThemeManagerScope : Scope { - override val hooks: List = listOf( - UnlockVideoRestrictionsHook(), - UnlockTemplateMaximumLimitHook(), - UnmuteVideoWallpaperHook(), - RearWallpaperThemeManagerSyncHook() - ) + override val hooks: List = buildList { + if (isRearDevice) { + addAll( + listOf( + UnlockVideoRestrictionsHook(), + UnlockTemplateMaximumLimitHook(), + UnmuteVideoWallpaperHook(), + RearWallpaperThemeManagerSyncHook() + ) + ) + } else { + YLog.debug("This device is not support rear screen, skip load some features that this device is not supported") + } + } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 9e232f2..9bef30b 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -734,11 +734,15 @@ private fun AboutRootContent( insideMargin = deviceInfoRowPadding, ) - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.subsceen_version), - summary = DeviceConfigTools.getSubSceenVersion(context), - insideMargin = deviceInfoRowPadding, - ) + val subscreenVer = DeviceConfigTools.getSubScreenVersion(context) + + if (subscreenVer != "UNKNOWN") { + BasicComponent( + title = androidx.compose.ui.res.stringResource(R.string.subsceen_version), + summary = subscreenVer, + insideMargin = deviceInfoRowPadding, + ) + } } } } diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt index 9e7f1d2..798061a 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt @@ -12,9 +12,9 @@ object DeviceConfigTools { val getdevice = if (hasRootAccess()) executeRootCommand("getprop persist.private.device_name") else Pair( 20, - "无法获取Root来获取设备名字" + "No Root Permission" ) - val deviceName = if (getdevice.first == 0) getdevice.second else "无法获取Root来获取设备名字" + val deviceName = if (getdevice.first == 0) getdevice.second else "No Root Permission" val androidVersion: String = getSystemProperties("ro.build.version.release") @@ -23,11 +23,11 @@ object DeviceConfigTools { val marketName: String = getSystemProperties("ro.product.marketname") - if (marketName.isNotEmpty()) bigtextone(marketName) else bigtextone(Build.BRAND) + " " + Build.MODEL + if (marketName.isNotEmpty()) titleFirstChar(marketName) else titleFirstChar(Build.BRAND) + " " + Build.MODEL } - fun bigtextone(st: String): String { + fun titleFirstChar(st: String): String { val formattedBrand = st.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } @@ -48,9 +48,13 @@ object DeviceConfigTools { return ret } - fun getSubSceenVersion(context: Context): String { + fun getSubScreenVersion(context: Context): String { val packageManager = context.packageManager - val packageInfo = packageManager.getPackageInfo("com.xiaomi.subscreencenter", 0) - return packageInfo.versionName.toString() + return try { + val packageInfo = packageManager.getPackageInfo("com.xiaomi.subscreencenter", 0) + packageInfo.versionName.toString() + } catch (_: Exception) { + "UNKNOWN" + } } } \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt index acabd50..869833b 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/OSVersionTools.kt @@ -41,7 +41,7 @@ object OSVersionTools { } if (!isXmsValid || isRoValid) { - xmsVersion = if (isXmsValid || !isRoValid) { + xmsVersion = if (isXmsValid) { compareValidVersion(xmsVersion, roXmsVersion) } else { roXmsVersion From e5fe9e2619c1fbf16fe43b2a3c72d89305b41ea8 Mon Sep 17 00:00:00 2001 From: AnserJim <81072191+killerprojecte@users.noreply.github.com> Date: Mon, 4 May 2026 23:00:40 +0800 Subject: [PATCH 12/27] chore: remove unused import Removed unused import statement for Log. --- .../main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt index e4f52f0..1f83a25 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/DeviceConfigTools.kt @@ -3,7 +3,6 @@ package hk.uwu.reareye.utils.other import android.annotation.SuppressLint import android.content.Context import android.os.Build -import android.util.Log import hk.uwu.reareye.utils.RootHelper.executeRootCommand import hk.uwu.reareye.utils.RootHelper.hasRootAccess @@ -58,4 +57,4 @@ object DeviceConfigTools { "UNKNOWN" } } -} \ No newline at end of file +} From a16532c5eb76977671295a0fccecb16fb5fc2674 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 4 May 2026 23:39:20 +0800 Subject: [PATCH 13/27] chore: move libraries reference card to page bottom & optimized about screen animation --- .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 617 +++++++++++------- 1 file changed, 378 insertions(+), 239 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index bb85649..a7bbd34 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -127,6 +127,7 @@ import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop import top.yukonga.miuix.kmp.blur.textureBlur import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Create +import top.yukonga.miuix.kmp.icon.extended.Info import top.yukonga.miuix.kmp.icon.extended.Link import top.yukonga.miuix.kmp.shapes.SmoothRoundedCornerShape import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @@ -186,6 +187,11 @@ private sealed interface AboutRoute { data object Licenses : AboutRoute } +private data class AboutAnimatedRoute( + val route: AboutRoute, + val depth: Int, +) + private data class CreditEntry( val titleRes: Int, val summaryRes: Int, @@ -255,27 +261,18 @@ private fun rememberSkeletonPulseAlpha(label: String): Float { @Composable fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { - val layoutDirection = LocalLayoutDirection.current - val scrollBehavior = MiuixScrollBehavior() - val hazeState = rememberAcrylicHazeState() - val hazeStyle = rememberAcrylicHazeStyle() val versionText = rememberVersionText() - var logoHeightPx by remember { mutableIntStateOf(0) } val contributorState by ContributorRepository.state.collectAsState() val lazyListState = rememberLazyListState() + var logoHeightPx by remember { mutableIntStateOf(0) } var route by remember { mutableStateOf(AboutRoute.Root) } - - val scrollProgress by remember { - derivedStateOf { - if (logoHeightPx <= 0) { - 0f - } else { - val index = lazyListState.firstVisibleItemIndex - val offset = lazyListState.firstVisibleItemScrollOffset - if (index > 0) 1f else (offset.toFloat() / logoHeightPx).coerceIn(0f, 1f) - } - } + var animateRootContent by remember { mutableStateOf(true) } + val animatedRoute = remember(route) { + AboutAnimatedRoute( + route = route, + depth = if (route is AboutRoute.Root) 0 else 1, + ) } val entries = remember { @@ -329,106 +326,84 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { route = AboutRoute.Root } - Scaffold( - topBar = { - if (route is AboutRoute.Root) { - SmallTopAppBar( - title = stringResource(R.string.about_navigation), - scrollBehavior = scrollBehavior, - color = colorScheme.surface.copy( - alpha = if (scrollProgress == 1f) 1f else 0f - ), - titleColor = colorScheme.onSurface.copy(alpha = scrollProgress), - defaultWindowInsetsPadding = false, - navigationIconPadding = 12.dp, + AnimatedContent( + modifier = Modifier + .fillMaxSize() + .background(colorScheme.surface) + .graphicsLayer { clip = true }, + targetState = animatedRoute, + contentKey = { it.route }, + transitionSpec = { + val forward = targetState.depth >= initialState.depth + + fadeIn( + animationSpec = tween( + durationMillis = 210, + delayMillis = 50, + easing = LinearOutSlowInEasing, ) - } else { - TopAppBar( - modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), - color = Color.Transparent, - title = if (route is AboutRoute.Contributors) { stringResource(R.string.credits_contributors_title) } else { stringResource(R.string.licenses_name)}, - navigationIcon = { - IconButton(onClick = { route = AboutRoute.Root }) { - Icon( - modifier = Modifier.graphicsLayer { - if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f - }, - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - ) - } - }, - navigationIconPadding = 12.dp, - scrollBehavior = scrollBehavior, + ) + slideInHorizontally( + animationSpec = tween( + durationMillis = 280, + easing = FastOutSlowInEasing, ) - } - } - ) { paddingValues -> - AnimatedContent( - modifier = Modifier - .fillMaxSize() - .background(colorScheme.surface) - .graphicsLayer { clip = true }, - targetState = route, - contentKey = { it }, - transitionSpec = { - val forward = targetState is AboutRoute.Contributors || targetState is AboutRoute.Licenses - - fadeIn( - animationSpec = tween( - durationMillis = 210, - delayMillis = 50, - easing = LinearOutSlowInEasing, - ) - ) + slideInHorizontally( - animationSpec = tween( - durationMillis = 280, - easing = FastOutSlowInEasing, - ) - ) { fullWidth -> - if (forward) fullWidth / 9 else -fullWidth / 9 - } togetherWith ( - fadeOut( - animationSpec = tween( - durationMillis = 110, - easing = FastOutLinearInEasing, - ) - ) + slideOutHorizontally( - animationSpec = tween( - durationMillis = 190, - easing = FastOutLinearInEasing, - ) - ) { fullWidth -> - if (forward) -fullWidth / 12 else fullWidth / 12 - } + ) { fullWidth -> + if (forward) fullWidth / 9 else -fullWidth / 9 + } togetherWith ( + fadeOut( + animationSpec = tween( + durationMillis = 110, + easing = FastOutLinearInEasing, ) - }, - label = "AboutRouteTransition", - ) { currentRoute -> - when (currentRoute) { - AboutRoute.Root -> AboutRootContent( - bottomInnerPadding = bottomInnerPadding, - paddingValues = paddingValues, - scrollBehavior = scrollBehavior, - hazeState = hazeState, - versionText = versionText, - entries = entries, - onOpenContributors = { route = AboutRoute.Contributors }, - onOpenLibraries = { route = AboutRoute.Licenses }, - lazyListState = lazyListState, - scrollProgress = scrollProgress, - onLogoHeightChanged = { logoHeightPx = it } - ) + ) + slideOutHorizontally( + animationSpec = tween( + durationMillis = 190, + easing = FastOutLinearInEasing, + ) + ) { fullWidth -> + if (forward) -fullWidth / 12 else fullWidth / 12 + } + ) + }, + label = "AboutRouteTransition", + ) { target -> + when (target.route) { + AboutRoute.Root -> AboutRootPage( + bottomInnerPadding = bottomInnerPadding, + versionText = versionText, + entries = entries, + lazyListState = lazyListState, + logoHeightPx = logoHeightPx, + animateEnter = animateRootContent, + onLogoHeightChanged = { logoHeightPx = it }, + onOpenContributors = { + animateRootContent = false + route = AboutRoute.Contributors + }, + onOpenLibraries = { + animateRootContent = false + route = AboutRoute.Licenses + }, + ) - AboutRoute.Contributors -> ContributorListContent( + AboutRoute.Contributors -> AboutSecondaryPage( + title = stringResource(R.string.credits_contributors_title), + onBack = { route = AboutRoute.Root }, + ) { paddingValues, scrollBehavior, hazeState -> + ContributorListContent( bottomInnerPadding = bottomInnerPadding, paddingValues = paddingValues, scrollBehavior = scrollBehavior, hazeState = hazeState, state = contributorState, ) + } - AboutRoute.Licenses -> LicenseContent( + AboutRoute.Licenses -> AboutSecondaryPage( + title = stringResource(R.string.licenses_name), + onBack = { route = AboutRoute.Root }, + ) { paddingValues, scrollBehavior, hazeState -> + LicenseContent( bottomInnerPadding = bottomInnerPadding, paddingValues = paddingValues, scrollBehavior = scrollBehavior, @@ -439,6 +414,105 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { } } +@Composable +private fun AboutRootPage( + bottomInnerPadding: Dp, + versionText: String, + entries: List, + lazyListState: LazyListState, + logoHeightPx: Int, + animateEnter: Boolean, + onLogoHeightChanged: (Int) -> Unit, + onOpenContributors: () -> Unit, + onOpenLibraries: () -> Unit, +) { + val scrollBehavior = MiuixScrollBehavior() + val hazeState = rememberAcrylicHazeState() + val scrollProgress by remember { + derivedStateOf { + val index = lazyListState.firstVisibleItemIndex + val offset = lazyListState.firstVisibleItemScrollOffset + + if (index > 0) { + 1f + } else if (logoHeightPx <= 0) { + 0f + } else { + (offset.toFloat() / logoHeightPx).coerceIn(0f, 1f) + } + } + } + + Scaffold( + topBar = { + SmallTopAppBar( + title = stringResource(R.string.about_navigation), + scrollBehavior = scrollBehavior, + color = colorScheme.surface.copy(alpha = if (scrollProgress == 1f) 1f else 0f), + titleColor = colorScheme.onSurface.copy(alpha = scrollProgress), + defaultWindowInsetsPadding = false, + navigationIconPadding = 12.dp, + ) + } + ) { paddingValues -> + AboutRootContent( + bottomInnerPadding = bottomInnerPadding, + paddingValues = paddingValues, + scrollBehavior = scrollBehavior, + hazeState = hazeState, + versionText = versionText, + entries = entries, + onOpenContributors = onOpenContributors, + scrollProgress = scrollProgress, + onLogoHeightChanged = onLogoHeightChanged, + lazyListState = lazyListState, + onOpenLibraries = onOpenLibraries, + animateEnter = animateEnter, + ) + } +} + +@Composable +private fun AboutSecondaryPage( + title: String, + onBack: () -> Unit, + content: @Composable (PaddingValues, ScrollBehavior, HazeState) -> Unit, +) { + val layoutDirection = LocalLayoutDirection.current + val scrollBehavior = MiuixScrollBehavior() + val hazeState = rememberAcrylicHazeState() + val hazeStyle = rememberAcrylicHazeStyle() + + Scaffold( + topBar = { + TopAppBar( + modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), + color = Color.Transparent, + title = title, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + modifier = Modifier.graphicsLayer { + if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f + }, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + navigationIconPadding = 12.dp, + scrollBehavior = scrollBehavior, + ) + } + ) { paddingValues -> + content( + paddingValues, + scrollBehavior, + hazeState, + ) + } +} + @Composable private fun AboutRootContent( bottomInnerPadding: Dp, @@ -452,6 +526,7 @@ private fun AboutRootContent( onLogoHeightChanged: (Int) -> Unit, lazyListState: LazyListState, onOpenLibraries: () -> Unit, + animateEnter: Boolean, ) { val context = LocalContext.current val visualTokens = rememberAboutVisualTokens() @@ -680,93 +755,20 @@ private fun AboutRootContent( .padding(bottom = scrollPadding.calculateBottomPadding()), verticalArrangement = Arrangement.spacedBy(AboutCardSpacing), ) { - ArtStaggeredReveal( - visible = true, - revealKey = "device_info", - delayMillis = 36, - ) { - val layoutDirection = LocalLayoutDirection.current - val deviceInfoCardPadding = PaddingValues( - start = BasicComponentDefaults.InsideMargin.calculateStartPadding( - layoutDirection - ), - top = AboutDeviceInfoCardTopPadding, - end = BasicComponentDefaults.InsideMargin.calculateEndPadding( - layoutDirection - ), - bottom = AboutDeviceInfoCardBottomPadding, - ) - val deviceInfoRowPadding = - PaddingValues(vertical = AboutDeviceInfoRowVerticalPadding) - - Card( - modifier = Modifier - .textureBlur( - backdrop = backdrop, - shape = SmoothRoundedCornerShape(16.dp), - blurRadius = 60f, - noiseCoefficient = 0.001f, - colors = BlurColors( - blendColors = visualTokens.cardBlendColors, - brightness = 0f, - contrast = 1f, - saturation = 1f, - ), - enabled = true, - ), - colors = CardDefaults.defaultColors( - Color.Transparent, - Color.Transparent, - ), - ) { - Column( - modifier = Modifier.padding(deviceInfoCardPadding), - ) { - Text( - text = DeviceConfigTools.deviceName, - modifier = Modifier.padding(bottom = AboutDeviceInfoHeaderBottomSpacing), - fontSize = 24.sp, - fontWeight = FontWeight.Medium, - color = BasicComponentDefaults.titleColor().color, - ) - - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.device_name), - summary = DeviceConfigTools.marketName, - insideMargin = deviceInfoRowPadding, - ) - - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.android_version), - summary = DeviceConfigTools.androidVersion, - insideMargin = deviceInfoRowPadding, - ) - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.os_version), - summary = OSVersionTools.addVersionSuffix(context), - insideMargin = deviceInfoRowPadding, - ) - - val subscreenVer = DeviceConfigTools.getSubScreenVersion(context) - - if (subscreenVer != "UNKNOWN") { - BasicComponent( - title = androidx.compose.ui.res.stringResource(R.string.subsceen_version), - summary = subscreenVer, - insideMargin = deviceInfoRowPadding, - ) - } - } - } - } + AboutDeviceInfoCard( + context = context, + backdrop = backdrop, + visualTokens = visualTokens, + animateEnter = animateEnter, + ) - ArtStaggeredReveal( - visible = true, - revealKey = "contributors", - delayMillis = 36, + Column( + verticalArrangement = Arrangement.spacedBy(AboutCardSpacing), ) { - Column( - verticalArrangement = Arrangement.spacedBy(AboutCardSpacing), + AboutReveal( + enabled = animateEnter, + revealKey = "contributors", + delayMillis = 36, ) { Card( modifier = Modifier @@ -800,60 +802,95 @@ private fun AboutRootContent( ) } ) - SuperCard( - title = stringResource(R.string.licenses_name), - summary = stringResource(R.string.licenses_name_summary), - onClick = onOpenLibraries, - ) } + } - entries.forEachIndexed { index, entry -> - ArtStaggeredReveal( - visible = true, - revealKey = entry.url, - delayMillis = (54 + index * 18).coerceAtMost(150), - ) { - Card( - modifier = Modifier - .textureBlur( - backdrop = backdrop, - shape = SmoothRoundedCornerShape(16.dp), - blurRadius = 60f, - noiseCoefficient = 0.001f, - colors = BlurColors( - blendColors = visualTokens.cardBlendColors, - brightness = 0f, - contrast = 1f, - saturation = 1f, - ), - enabled = true, + entries.forEachIndexed { index, entry -> + AboutReveal( + enabled = animateEnter, + revealKey = entry.url, + delayMillis = (54 + index * 18).coerceAtMost(150), + ) { + Card( + modifier = Modifier + .textureBlur( + backdrop = backdrop, + shape = SmoothRoundedCornerShape(16.dp), + blurRadius = 60f, + noiseCoefficient = 0.001f, + colors = BlurColors( + blendColors = visualTokens.cardBlendColors, + brightness = 0f, + contrast = 1f, + saturation = 1f, ), - colors = CardDefaults.defaultColors( - Color.Transparent, - Color.Transparent, + enabled = true, ), - ) { - SuperCard( - title = stringResource(entry.titleRes), - summary = stringResource(entry.summaryRes), - onClick = { - context.startActivity( - Intent( - Intent.ACTION_VIEW, - entry.url.toUri() - ) - ) - }, - endActions = { - Icon( - imageVector = MiuixIcons.Link, - tint = colorScheme.onSurface, - contentDescription = null + colors = CardDefaults.defaultColors( + Color.Transparent, + Color.Transparent, + ), + ) { + SuperCard( + title = stringResource(entry.titleRes), + summary = stringResource(entry.summaryRes), + onClick = { + context.startActivity( + Intent( + Intent.ACTION_VIEW, + entry.url.toUri() ) - } + ) + }, + endActions = { + Icon( + imageVector = MiuixIcons.Link, + tint = colorScheme.onSurface, + contentDescription = null + ) + } + ) + } + } + } + + AboutReveal( + enabled = animateEnter, + revealKey = "licenses", + delayMillis = (54 + entries.size * 18).coerceAtMost(150), + ) { + Card( + modifier = Modifier + .textureBlur( + backdrop = backdrop, + shape = SmoothRoundedCornerShape(16.dp), + blurRadius = 60f, + noiseCoefficient = 0.001f, + colors = BlurColors( + blendColors = visualTokens.cardBlendColors, + brightness = 0f, + contrast = 1f, + saturation = 1f, + ), + enabled = true, + ), + colors = CardDefaults.defaultColors( + Color.Transparent, + Color.Transparent, + ), + ) { + SuperCard( + title = stringResource(R.string.licenses_name), + summary = stringResource(R.string.licenses_name_summary), + onClick = onOpenLibraries, + endActions = { + Icon( + imageVector = MiuixIcons.Info, + tint = colorScheme.onSurface, + contentDescription = null ) } - } + ) } } } @@ -864,6 +901,108 @@ private fun AboutRootContent( } } +@Composable +private fun AboutDeviceInfoCard( + context: android.content.Context, + backdrop: top.yukonga.miuix.kmp.blur.LayerBackdrop, + visualTokens: AboutVisualTokens, + animateEnter: Boolean, +) { + AboutReveal( + enabled = animateEnter, + revealKey = "device_info_card", + delayMillis = 0, + ) { + val layoutDirection = LocalLayoutDirection.current + val deviceInfoCardPadding = PaddingValues( + start = BasicComponentDefaults.InsideMargin.calculateStartPadding(layoutDirection), + top = AboutDeviceInfoCardTopPadding, + end = BasicComponentDefaults.InsideMargin.calculateEndPadding(layoutDirection), + bottom = AboutDeviceInfoCardBottomPadding, + ) + val deviceInfoRowPadding = PaddingValues(vertical = AboutDeviceInfoRowVerticalPadding) + val subscreenVer = DeviceConfigTools.getSubScreenVersion(context) + + Card( + modifier = Modifier + .textureBlur( + backdrop = backdrop, + shape = SmoothRoundedCornerShape(16.dp), + blurRadius = 60f, + noiseCoefficient = 0.001f, + colors = BlurColors( + blendColors = visualTokens.cardBlendColors, + brightness = 0f, + contrast = 1f, + saturation = 1f, + ), + enabled = true, + ), + colors = CardDefaults.defaultColors( + Color.Transparent, + Color.Transparent, + ), + ) { + Column( + modifier = Modifier.padding(deviceInfoCardPadding), + ) { + Text( + text = DeviceConfigTools.deviceName, + modifier = Modifier.padding(bottom = AboutDeviceInfoHeaderBottomSpacing), + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + color = BasicComponentDefaults.titleColor().color, + ) + + BasicComponent( + title = stringResource(R.string.device_name), + summary = DeviceConfigTools.marketName, + insideMargin = deviceInfoRowPadding, + ) + + BasicComponent( + title = stringResource(R.string.android_version), + summary = DeviceConfigTools.androidVersion, + insideMargin = deviceInfoRowPadding, + ) + + BasicComponent( + title = stringResource(R.string.os_version), + summary = OSVersionTools.addVersionSuffix(context), + insideMargin = deviceInfoRowPadding, + ) + + if (subscreenVer != "UNKNOWN") { + BasicComponent( + title = stringResource(R.string.subsceen_version), + summary = subscreenVer, + insideMargin = deviceInfoRowPadding, + ) + } + } + } + } +} + +@Composable +private fun AboutReveal( + enabled: Boolean, + revealKey: Any, + delayMillis: Int, + content: @Composable () -> Unit, +) { + if (enabled) { + ArtStaggeredReveal( + visible = true, + revealKey = revealKey, + delayMillis = delayMillis, + content = content, + ) + } else { + content() + } +} + @Composable private fun ContributorListContent( bottomInnerPadding: Dp, @@ -973,7 +1112,7 @@ private fun LicenseContent( paddingValues: PaddingValues, scrollBehavior: ScrollBehavior, hazeState: HazeState, -){ +) { val context = LocalContext.current val data = remember { loadLibraries(context) @@ -994,7 +1133,7 @@ private fun LicenseContent( ), verticalArrangement = Arrangement.spacedBy(8.dp), overscrollEffect = null, - ){ + ) { items(data.libraries) { LibraryItem(it) } From 7d026cf0fbba5ac8170e20a577f8b6e10123c231 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Tue, 5 May 2026 23:03:00 +0800 Subject: [PATCH 14/27] feat: widget info add requirements (configs & packages) & post-install grammar --- app/src/main/AndroidManifest.xml | 6 + .../RearStoreArchiveContentProvider.kt | 113 +++ .../rearstore/RearStoreRepository.kt | 717 +++++++++++++++++- .../hk/uwu/reareye/service/BaseTileService.kt | 3 - .../hk/uwu/reareye/ui/config/PrefsManager.kt | 8 +- .../uwu/reareye/ui/screen/RearStoreScreen.kt | 292 ++++++- app/src/main/res/values-zh-rCN/strings.xml | 22 + app/src/main/res/values/strings.xml | 22 + 8 files changed, 1149 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/hk/uwu/reareye/provider/RearStoreArchiveContentProvider.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19fd968..bcb5ede 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,6 +60,12 @@ + + ?, + selection: String?, + selectionArgs: Array?, + sortOrder: String?, + ): Cursor { + val context = context ?: return singleJsonCursor(buildErrorPayload("Context unavailable")) + val prefsManager = context.applicationContext.getPrefsManager() + val mode = uri.getQueryParameter("mode")?.trim().orEmpty() + val entry = uri.getQueryParameter("entry")?.trim()?.ifBlank { null } + val payload = runCatching { + val source = when (mode) { + "store_id" -> { + val widgetId = uri.getQueryParameter("id")?.trim().orEmpty() + require(widgetId.isNotBlank()) { "Missing required query parameter: id" } + RearStoreRepository.resolveInstalledArchiveSourceByStoreWidgetId( + prefsManager, + widgetId + ) + ?: error("Installed store widget not found") + } + + "business_id" -> { + val businessId = uri.getQueryParameter("id")?.trim().orEmpty() + require(businessId.isNotBlank()) { "Missing required query parameter: id" } + RearStoreRepository.resolveInstalledArchiveSourceByBusinessConfigId( + prefsManager, + businessId, + ) ?: error("Installed business config not found") + } + + else -> error("Unsupported mode: $mode") + } + + JSONObject().apply { + put("success", true) + put("error", JSONObject.NULL) + put("mode", mode) + put("storeWidgetId", source.storeWidgetId) + put("businessConfigId", source.businessConfigId) + put("business", source.businessName) + put("card", source.cardId) + put("entry", entry) + if (entry == null) { + put( + "entries", + JSONArray(RearStoreRepository.listInstalledArchiveEntries(source.filePath)) + ) + put("contentBase64", JSONObject.NULL) + } else { + put( + "contentBase64", + RearStoreRepository.readInstalledArchiveFileBase64( + filePath = source.filePath, + entryName = entry, + ) + ) + } + } + }.getOrElse { error -> + buildErrorPayload(error.message ?: "Unknown error") + } + + return singleJsonCursor(payload) + } + + override fun getType(uri: Uri): String { + return "application/json" + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array?, + ): Int = 0 + + private fun singleJsonCursor(payload: JSONObject): Cursor { + return MatrixCursor(arrayOf("json")).apply { + addRow(arrayOf(payload.toString())) + } + } + + private fun buildErrorPayload(message: String): JSONObject { + return JSONObject().apply { + put("success", false) + put("error", message) + put("contentBase64", JSONObject.NULL) + put("entries", JSONObject.NULL) + } + } +} diff --git a/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt b/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt index 1a77fb3..34123f2 100644 --- a/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt +++ b/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt @@ -1,11 +1,15 @@ package hk.uwu.reareye.repository.rearstore import android.content.Context +import android.content.pm.PackageManager import android.net.Uri +import android.util.Base64 import androidx.annotation.Keep +import androidx.core.content.ContextCompat import com.google.gson.Gson import com.google.gson.annotations.SerializedName import hk.uwu.reareye.BuildConfig +import hk.uwu.reareye.R import hk.uwu.reareye.repository.rearwallpaper.RearWallpaperMetadataOptions import hk.uwu.reareye.repository.rearwallpaper.RearWallpaperOperationResult import hk.uwu.reareye.repository.rearwallpaper.RearWallpaperRepository @@ -14,8 +18,14 @@ import hk.uwu.reareye.repository.rearwidget.RearCardConfig import hk.uwu.reareye.repository.rearwidget.RearWidgetConfigCodec import hk.uwu.reareye.repository.rearwidget.RearWidgetManagerRepository import hk.uwu.reareye.repository.rearwidget.RearWidgetSceneRouteConfig +import hk.uwu.reareye.ui.config.ConfigCategory +import hk.uwu.reareye.ui.config.ConfigGroup +import hk.uwu.reareye.ui.config.ConfigItem import hk.uwu.reareye.ui.config.ConfigKeys +import hk.uwu.reareye.ui.config.ConfigNode +import hk.uwu.reareye.ui.config.ConfigType import hk.uwu.reareye.ui.config.PrefsManager +import hk.uwu.reareye.ui.config.REAREyeConfig import hk.uwu.reareye.ui.config.resolveRearStoreApiBaseUrl import hk.uwu.reareye.widgetapi.RearWidgetSceneRouteSpec import kotlinx.coroutines.Dispatchers @@ -31,15 +41,69 @@ import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONArray import org.json.JSONObject import java.io.ByteArrayOutputStream +import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import java.util.zip.ZipFile import java.util.zip.ZipInputStream private const val DEFAULT_COMPONENT_ROUTE_PACKAGE = "com.xiaomi.subscreencenter" +private const val MIUI_GET_INSTALLED_APPS_PERMISSION = "com.android.permission.GET_INSTALLED_APPS" private const val WIDGET_VERSION_CHECK_DISABLED = -1L +@Keep +data class RearStoreWidgetRequirements( + @SerializedName("packages") + val packages: List = emptyList(), + @SerializedName("configs") + val configs: Map = emptyMap(), +) + +@Keep +data class RearStorePostInstall( + @SerializedName("uri") + val uri: String = "", +) + +data class RearStoreWidgetRequirementsResult( + val satisfied: Boolean, + val appListPermissionGranted: Boolean = true, + val missingPackages: List = emptyList(), + val failedConfigKeys: List = emptyList(), + val failedConfigRequirements: Map = emptyMap(), + val failedConfigReasons: Map = emptyMap(), +) + +private data class RearStoreConfigNodeIndex( + val titleResByKey: Map, + val itemTypeByKey: Map, +) + +private enum class RearStoreRequirementOperator(val token: String) { + GTE(">="), + LTE("<="), + EQ("=="), + NE("!="), + GT(">"), + LT("<"); + + companion object { + private val ordered = entries.sortedByDescending { it.token.length } + + fun parse(expression: String): Pair { + val normalized = expression.trim() + ordered.forEach { operator -> + if (normalized.startsWith(operator.token)) { + return operator to normalized.removePrefix(operator.token).trim() + } + } + return EQ to normalized + } + } +} + private fun String?.normalizedOrNull(): String? { return this?.trim()?.takeIf { it.isNotEmpty() } } @@ -94,10 +158,475 @@ fun RearStoreWidgetInfo?.supportsModuleVersion( (maxVersion == WIDGET_VERSION_CHECK_DISABLED || versionCode <= maxVersion) } +fun RearStoreWidgetInfo?.evaluateRequirements( + context: Context, + prefsManager: PrefsManager, +): RearStoreWidgetRequirementsResult { + val requirements = this?.requirements + val requiredPackages = requirements.normalizedPackages() + val requiredConfigs = requirements.normalizedConfigExpressions() + val configNodeIndex = buildConfigNodeIndex() + if (requiredPackages.isEmpty() && requiredConfigs.isEmpty()) { + return RearStoreWidgetRequirementsResult(satisfied = true) + } + + val installedPackages = if (requiredPackages.isEmpty()) { + emptySet() + } else { + loadInstalledPackagesOrNull(context) + } + val missingPackages = when { + requiredPackages.isEmpty() -> emptyList() + installedPackages == null -> requiredPackages + else -> requiredPackages.filterNot(installedPackages::contains) + } + val failedConfigReasons = linkedMapOf() + requiredConfigs.forEach { (key, expression) -> + if (!configNodeIndex.titleResByKey.containsKey(key)) { + failedConfigReasons[key] = context.getString( + R.string.rear_store_requirement_config_unknown_item, + key, + ) + return@forEach + } + val actualValue = prefsManager.getRequirementValue(key) + val result = evaluateConfigRequirement( + context = context, + key = key, + actualValue = actualValue, + expression = expression, + configNodeIndex = configNodeIndex, + ) + if (!result.satisfied) { + failedConfigReasons[key] = result.reason + } + } + val failedConfigKeys = failedConfigReasons.keys.toList() + + return RearStoreWidgetRequirementsResult( + satisfied = missingPackages.isEmpty() && failedConfigKeys.isEmpty(), + appListPermissionGranted = requiredPackages.isEmpty() || installedPackages != null, + missingPackages = missingPackages, + failedConfigKeys = failedConfigKeys, + failedConfigRequirements = requiredConfigs.filterKeys(failedConfigKeys::contains), + failedConfigReasons = failedConfigReasons, + ) +} + fun RearStoreWidgetMetadata?.resolvedType(): RearStoreWidgetMetadataType { return RearStoreWidgetMetadataType.fromRaw(this?.type) } +private fun RearStoreWidgetRequirements?.normalizedPackages(): List { + return this?.packages + ?.mapNotNull(String::normalizedOrNull) + ?.distinct() + .orEmpty() +} + +private fun RearStoreWidgetRequirements?.normalizedConfigExpressions(): Map { + return this?.configs + ?.mapNotNull { (key, value) -> + val normalizedKey = key.normalizedOrNull() ?: return@mapNotNull null + val expression = value.toRequirementExpression() ?: return@mapNotNull null + normalizedKey to expression + } + ?.toMap(linkedMapOf()) + .orEmpty() +} + +private fun Any?.toRequirementExpression(): String? { + return when (this) { + null -> null + is String -> normalizedOrNull() + is Boolean, is Number -> "== $this" + is Collection<*> -> null + else -> toString().normalizedOrNull() + } +} + +private fun loadInstalledPackagesOrNull(context: Context): Set? { + val canReadAppList = ContextCompat.checkSelfPermission( + context, + MIUI_GET_INSTALLED_APPS_PERMISSION, + ) == PackageManager.PERMISSION_GRANTED + if (!canReadAppList) return null + return runCatching { + context.packageManager + .getInstalledApplications(PackageManager.GET_META_DATA) + .mapNotNullTo(linkedSetOf()) { it.packageName.normalizedOrNull() } + }.getOrNull() +} + +private data class RearStoreRequirementCheckResult( + val satisfied: Boolean, + val reason: String, +) + +private fun evaluateConfigRequirement( + context: Context, + key: String, + actualValue: Any?, + expression: String, + configNodeIndex: RearStoreConfigNodeIndex, +): RearStoreRequirementCheckResult { + if (actualValue == null) { + return RearStoreRequirementCheckResult( + satisfied = false, + reason = buildConfigMissingReason(context, key, configNodeIndex), + ) + } + val (operator, operand) = RearStoreRequirementOperator.parse(expression) + return when (actualValue) { + is Boolean -> evaluateBooleanRequirement( + context, + key, + actualValue, + operator, + operand, + configNodeIndex + ) + + is Int -> evaluateNumericRequirement( + context, + key, + actualValue.toDouble(), + operator, + operand, + configNodeIndex + ) + + is Long -> evaluateNumericRequirement( + context, + key, + actualValue.toDouble(), + operator, + operand, + configNodeIndex + ) + + is Float -> evaluateNumericRequirement( + context, + key, + actualValue.toDouble(), + operator, + operand, + configNodeIndex + ) + + is Double -> evaluateNumericRequirement( + context, + key, + actualValue, + operator, + operand, + configNodeIndex + ) + + is Set<*> -> evaluateCollectionRequirement( + context, + key, + actualValue, + operator, + operand, + configNodeIndex + ) + + is Collection<*> -> evaluateCollectionRequirement( + context, + key, + actualValue, + operator, + operand, + configNodeIndex + ) + + else -> evaluateStringRequirement( + context, + key, + actualValue.toString(), + operator, + operand, + configNodeIndex + ) + } +} + +private fun evaluateBooleanRequirement( + context: Context, + key: String, + actual: Boolean, + operator: RearStoreRequirementOperator, + operand: String, + configNodeIndex: RearStoreConfigNodeIndex, +): RearStoreRequirementCheckResult { + val expected = operand.toBooleanStrictOrNull() + val satisfied = when (operator) { + RearStoreRequirementOperator.EQ -> expected != null && actual == expected + RearStoreRequirementOperator.NE -> expected != null && actual != expected + else -> false + } + if (satisfied) return RearStoreRequirementCheckResult(true, "") + + val reason = when { + expected == true && operator == RearStoreRequirementOperator.EQ -> { + buildConfigNeedEnabledReason(context, key, configNodeIndex) + } + + expected == false && operator == RearStoreRequirementOperator.EQ -> { + buildConfigNeedDisabledReason(context, key, configNodeIndex) + } + + expected == true && operator == RearStoreRequirementOperator.NE -> { + buildConfigNotEnabledReason(context, key, configNodeIndex) + } + + expected == false && operator == RearStoreRequirementOperator.NE -> { + buildConfigNotDisabledReason(context, key, configNodeIndex) + } + + else -> buildConfigNeedEnabledReason(context, key, configNodeIndex) + } + return RearStoreRequirementCheckResult(false, reason) +} + +private fun buildConfigNodeIndex(): RearStoreConfigNodeIndex { + val titleResByKey = linkedMapOf() + val itemTypeByKey = linkedMapOf() + + fun walk(nodes: List) { + nodes.forEach { node -> + when (node) { + is ConfigItem -> { + titleResByKey[node.key] = node.titleRes + itemTypeByKey[node.key] = node.type + } + + is ConfigCategory -> walk(node.children) + is ConfigGroup -> walk(node.children) + } + } + } + + walk(REAREyeConfig) + return RearStoreConfigNodeIndex(titleResByKey = titleResByKey, itemTypeByKey = itemTypeByKey) +} + +private fun buildConfigMissingReason( + context: Context, + key: String, + configNodeIndex: RearStoreConfigNodeIndex, +): String { + val titleRes = configNodeIndex.titleResByKey[key] + return if (titleRes != null) { + context.getString( + R.string.rear_store_requirement_config_missing_named_item, + quotedConfigTitle(context.getString(titleRes)), + ) + } else { + context.getString(R.string.rear_store_requirement_config_unknown_item, key) + } +} + +private fun quotedConfigTitle(title: String): String { + return "\"$title\"" +} + +private fun buildConfigNeedEnabledReason( + context: Context, + key: String, + configNodeIndex: RearStoreConfigNodeIndex, +): String { + val title = + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty() + return context.getString(R.string.rear_store_requirement_config_need_enabled_named_item, title) +} + +private fun buildConfigNeedDisabledReason( + context: Context, + key: String, + configNodeIndex: RearStoreConfigNodeIndex, +): String { + val title = + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty() + return context.getString(R.string.rear_store_requirement_config_need_disabled_named_item, title) +} + +private fun buildConfigNotEnabledReason( + context: Context, + key: String, + configNodeIndex: RearStoreConfigNodeIndex, +): String { + val title = + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty() + return context.getString(R.string.rear_store_requirement_config_not_enabled_named_item, title) +} + +private fun buildConfigNotDisabledReason( + context: Context, + key: String, + configNodeIndex: RearStoreConfigNodeIndex, +): String { + val title = + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty() + return context.getString(R.string.rear_store_requirement_config_not_disabled_named_item, title) +} + +private fun evaluateNumericRequirement( + context: Context, + key: String, + actual: Double, + operator: RearStoreRequirementOperator, + operand: String, + configNodeIndex: RearStoreConfigNodeIndex, +): RearStoreRequirementCheckResult { + val expected = operand.toDoubleOrNull() + val satisfied = when (operator) { + RearStoreRequirementOperator.GTE -> expected != null && actual >= expected + RearStoreRequirementOperator.LTE -> expected != null && actual <= expected + RearStoreRequirementOperator.EQ -> expected != null && actual == expected + RearStoreRequirementOperator.NE -> expected != null && actual != expected + RearStoreRequirementOperator.GT -> expected != null && actual > expected + RearStoreRequirementOperator.LT -> expected != null && actual < expected + } + if (satisfied) return RearStoreRequirementCheckResult(true, "") + + val reason = when (operator) { + RearStoreRequirementOperator.GTE -> context.getString( + R.string.rear_store_requirement_config_number_gte_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + + RearStoreRequirementOperator.LTE -> context.getString( + R.string.rear_store_requirement_config_number_lte_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + + RearStoreRequirementOperator.EQ -> context.getString( + R.string.rear_store_requirement_config_number_eq_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + + RearStoreRequirementOperator.NE -> context.getString( + R.string.rear_store_requirement_config_number_ne_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + + RearStoreRequirementOperator.GT -> context.getString( + R.string.rear_store_requirement_config_number_gt_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + + RearStoreRequirementOperator.LT -> context.getString( + R.string.rear_store_requirement_config_number_lt_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + } + return RearStoreRequirementCheckResult(false, reason) +} + +private fun evaluateCollectionRequirement( + context: Context, + key: String, + actual: Collection<*>, + operator: RearStoreRequirementOperator, + operand: String, + configNodeIndex: RearStoreConfigNodeIndex, +): RearStoreRequirementCheckResult { + val normalizedValues = actual.mapNotNull { it?.toString().normalizedOrNull() }.toSet() + val target = operand.normalizedOrNull() ?: return RearStoreRequirementCheckResult( + satisfied = false, + reason = buildConfigNeedEnabledReason(context, key, configNodeIndex), + ) + val satisfied = when (operator) { + RearStoreRequirementOperator.EQ -> target in normalizedValues + RearStoreRequirementOperator.NE -> target !in normalizedValues + else -> false + } + if (satisfied) return RearStoreRequirementCheckResult(true, "") + + val reason = when (operator) { + RearStoreRequirementOperator.EQ -> context.getString( + R.string.rear_store_requirement_config_contains_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + target, + ) + + RearStoreRequirementOperator.NE -> context.getString( + R.string.rear_store_requirement_config_not_contains_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + target, + ) + + else -> context.getString( + R.string.rear_store_requirement_config_contains_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + target, + ) + } + return RearStoreRequirementCheckResult(false, reason) +} + +private fun evaluateStringRequirement( + context: Context, + key: String, + actual: String, + operator: RearStoreRequirementOperator, + operand: String, + configNodeIndex: RearStoreConfigNodeIndex, +): RearStoreRequirementCheckResult { + val normalizedActual = actual.trim() + val satisfied = when (operator) { + RearStoreRequirementOperator.EQ -> normalizedActual == operand + RearStoreRequirementOperator.NE -> normalizedActual != operand + else -> false + } + if (satisfied) return RearStoreRequirementCheckResult(true, "") + + val reason = when (operator) { + RearStoreRequirementOperator.EQ -> context.getString( + R.string.rear_store_requirement_config_string_eq_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + + RearStoreRequirementOperator.NE -> context.getString( + R.string.rear_store_requirement_config_string_ne_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + + else -> context.getString( + R.string.rear_store_requirement_config_string_eq_item, + configNodeIndex.titleResByKey[key]?.let(context::getString)?.let(::quotedConfigTitle) + .orEmpty(), + operand, + ) + } + return RearStoreRequirementCheckResult(false, reason) +} + private fun parseMetadataType(rawType: String?): RearStoreWidgetMetadataType { val raw = rawType?.trim().orEmpty() if (raw.contains("壁纸")) return RearStoreWidgetMetadataType.WALLPAPER @@ -298,6 +827,10 @@ data class RearStoreWidgetInfo( val minVersion: Long = WIDGET_VERSION_CHECK_DISABLED, @SerializedName(value = "maxVersion", alternate = ["max_version"]) val maxVersion: Long = WIDGET_VERSION_CHECK_DISABLED, + @SerializedName("requirements") + val requirements: RearStoreWidgetRequirements? = null, + @SerializedName("postinstall") + val postInstall: RearStorePostInstall? = null, ) @Keep @@ -435,6 +968,16 @@ data class RearStoreQuickInstallResult( val cardInstalled: Boolean, val fallbackUsed: Boolean, val updatedExistingInstall: Boolean, + val businessConfigId: String? = null, + val cardId: String? = null, +) + +data class RearStoreInstalledArchiveSource( + val storeWidgetId: String?, + val businessConfigId: String, + val businessName: String, + val cardId: String?, + val filePath: String, ) data class RearStorePreparedInstallAsset( @@ -693,6 +1236,85 @@ object RearStoreRepository { } } + fun resolveInstalledArchiveSourceByStoreWidgetId( + prefsManager: PrefsManager, + widgetId: String, + ): RearStoreInstalledArchiveSource? { + val normalizedWidgetId = widgetId.normalizedOrNull() ?: return null + val businesses = RearWidgetManagerRepository.loadBusinesses(prefsManager) + val cards = RearWidgetManagerRepository.loadCards(prefsManager) + val business = businesses.firstOrNull { + it.downloadedFromStore && + it.storeWidgetId.normalizedOrNull() == normalizedWidgetId && + it.filePath.normalizedOrNull() != null + } ?: return null + val cardId = cards.firstOrNull { + it.downloadedFromStore && it.storeWidgetId.normalizedOrNull() == normalizedWidgetId + }?.id + return RearStoreInstalledArchiveSource( + storeWidgetId = normalizedWidgetId, + businessConfigId = business.id, + businessName = business.business, + cardId = cardId, + filePath = business.filePath, + ) + } + + fun resolveInstalledArchiveSourceByBusinessConfigId( + prefsManager: PrefsManager, + businessConfigId: String, + ): RearStoreInstalledArchiveSource? { + val normalizedBusinessConfigId = businessConfigId.normalizedOrNull() ?: return null + val businesses = RearWidgetManagerRepository.loadBusinesses(prefsManager) + val cards = RearWidgetManagerRepository.loadCards(prefsManager) + val business = businesses.firstOrNull { + it.id.normalizedOrNull() == normalizedBusinessConfigId && it.filePath.normalizedOrNull() != null + } ?: return null + val cardId = cards.firstOrNull { + it.business == business.business && + it.storeWidgetId.normalizedOrNull() == business.storeWidgetId.normalizedOrNull() + }?.id + return RearStoreInstalledArchiveSource( + storeWidgetId = business.storeWidgetId.normalizedOrNull(), + businessConfigId = business.id, + businessName = business.business, + cardId = cardId, + filePath = business.filePath, + ) + } + + fun readInstalledArchiveFileBase64( + filePath: String, + entryName: String? = null, + ): String { + val targetFile = File(filePath) + require(targetFile.exists() && targetFile.isFile) { "Archive file not found" } + val bytes = if (entryName.normalizedOrNull() == null) { + targetFile.readBytes() + } else { + readZipEntryBytes(targetFile, entryName.normalizedOrNull()!!) + ?: error("Entry '$entryName' not found in archive") + } + return Base64.encodeToString(bytes, Base64.NO_WRAP) + } + + fun listInstalledArchiveEntries(filePath: String): List { + val targetFile = File(filePath) + require(targetFile.exists() && targetFile.isFile) { "Archive file not found" } + return runCatching { + ZipFile(targetFile).use { zipFile -> + buildList { + val entries = zipFile.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (entry.isDirectory) continue + add(entry.name.replace('\\', '/').trimStart('/')) + } + }.sorted() + } + }.getOrElse { emptyList() } + } + fun loadInstalledWallpaperSources( prefsManager: PrefsManager, ): Map { @@ -904,6 +1526,36 @@ object RearStoreRepository { if (!detail.widgetInfo.supportsModuleVersion()) { error("Current module version does not support installing this component") } + val requirementsResult = detail.widgetInfo.evaluateRequirements(context, prefsManager) + if (!requirementsResult.satisfied) { + when { + !requirementsResult.appListPermissionGranted && requirementsResult.missingPackages.isNotEmpty() -> { + error("Installed apps permission is required to verify widget package requirements") + } + + requirementsResult.missingPackages.isNotEmpty() -> { + error( + "Missing required packages: ${ + requirementsResult.missingPackages.joinToString( + ", " + ) + }" + ) + } + + requirementsResult.failedConfigKeys.isNotEmpty() -> { + error( + "Unsatisfied required configs: ${ + requirementsResult.failedConfigKeys.joinToString( + ", " + ) + }" + ) + } + + else -> error("Widget requirements are not satisfied") + } + } val conflict = resolveInstallConflict(prefsManager, detail) if (conflict != null && !forceOverwrite) { error(buildInstallConflictMessage(conflict)) @@ -1001,34 +1653,33 @@ object RearStoreRepository { (conflictingStoreWidgetId != null && it.storeWidgetId.normalizedOrNull() == conflictingStoreWidgetId) } + val installedBusiness = RearBusinessConfig( + id = previousBusiness?.id + ?: RearWidgetConfigCodec.newBusinessId( + DEFAULT_COMPONENT_ROUTE_PACKAGE, + businessId, + ), + packageName = DEFAULT_COMPONENT_ROUTE_PACKAGE, + business = businessId, + filePath = targetPath, + defaultIndex = previousBusiness?.defaultIndex ?: 0, + defaultPriority = previousBusiness?.defaultPriority ?: 500, + renameable = businessSetup?.renameable ?: true, + downloadedFromStore = true, + storeWidgetId = detail.widgetId, + storeWidgetName = businessName, + storeReleaseTag = storeReleaseTag, + storeReleaseAssetName = storeReleaseAssetName, + storeReleasePublishedAt = storeReleasePublishedAt, + storeInstalledAt = installedAt, + ) val nextBusinesses = businesses .filterNot { it.matchesStoreBusiness(detail.widgetId, businessId) || (conflictingStoreWidgetId != null && it.storeWidgetId.normalizedOrNull() == conflictingStoreWidgetId) } - .plus( - RearBusinessConfig( - id = previousBusiness?.id - ?: RearWidgetConfigCodec.newBusinessId( - DEFAULT_COMPONENT_ROUTE_PACKAGE, - businessId, - ), - packageName = DEFAULT_COMPONENT_ROUTE_PACKAGE, - business = businessId, - filePath = targetPath, - defaultIndex = previousBusiness?.defaultIndex ?: 0, - defaultPriority = previousBusiness?.defaultPriority ?: 500, - renameable = businessSetup?.renameable ?: true, - downloadedFromStore = true, - storeWidgetId = detail.widgetId, - storeWidgetName = businessName, - storeReleaseTag = storeReleaseTag, - storeReleaseAssetName = storeReleaseAssetName, - storeReleasePublishedAt = storeReleasePublishedAt, - storeInstalledAt = installedAt, - ) - ) + .plus(installedBusiness) RearWidgetManagerRepository.saveBusinesses( context = context, prefsManager = prefsManager, @@ -1097,6 +1748,7 @@ object RearStoreRepository { } } var cardInstalled = false + var installedCardId: String? = null val nextCards = cards .filterNot { (cardPackage != null && it.matchesStoreCard( @@ -1111,7 +1763,7 @@ object RearStoreRepository { val cardSetup = detail.widgetInfo?.cardSetup ?: return@let existingCards val normalizedCardPackage = cardPackage ?: return@let existingCards cardInstalled = true - existingCards + RearCardConfig( + val installedCard = RearCardConfig( id = previousCard?.id ?: RearWidgetConfigCodec.newCardId(), title = cardSetup.name.normalizedOrNull() ?: businessName, packageName = normalizedCardPackage, @@ -1133,6 +1785,8 @@ object RearStoreRepository { storeReleaseAssetName = storeReleaseAssetName, storeReleasePublishedAt = storeReleasePublishedAt, ) + installedCardId = installedCard.id + existingCards + installedCard } if (nextCards != cards) { RearWidgetManagerRepository.saveCards( @@ -1150,6 +1804,8 @@ object RearStoreRepository { cardInstalled = cardInstalled, fallbackUsed = false, updatedExistingInstall = previousBusiness != null || conflict != null, + businessConfigId = installedBusiness.id, + cardId = installedCardId, ) } @@ -1222,6 +1878,8 @@ object RearStoreRepository { cardInstalled = false, fallbackUsed = false, updatedExistingInstall = previousRecords.isNotEmpty(), + businessConfigId = null, + cardId = null, ) } @@ -1390,6 +2048,19 @@ object RearStoreRepository { return optString(key).trim().takeIf { it.isNotBlank() } } + private fun readZipEntryBytes(file: File, entryName: String): ByteArray? { + val normalizedEntryName = entryName.replace('\\', '/').trimStart('/') + return runCatching { + ZipFile(file).use { zipFile -> + val entry = zipFile.entries().asSequence().firstOrNull { + !it.isDirectory && it.name.replace('\\', '/') + .trimStart('/') == normalizedEntryName + } ?: return@runCatching null + zipFile.getInputStream(entry).use { input -> input.readBytes() } + } + }.getOrNull() + } + private suspend fun fetchBytes( url: String, onProgress: (RearStoreInstallProgress) -> Unit, diff --git a/app/src/main/java/hk/uwu/reareye/service/BaseTileService.kt b/app/src/main/java/hk/uwu/reareye/service/BaseTileService.kt index 1b1feec..475af52 100644 --- a/app/src/main/java/hk/uwu/reareye/service/BaseTileService.kt +++ b/app/src/main/java/hk/uwu/reareye/service/BaseTileService.kt @@ -1,17 +1,14 @@ package hk.uwu.reareye.service -import android.os.Build import android.os.Handler import android.os.Looper import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log import android.widget.Toast -import androidx.annotation.RequiresApi import hk.uwu.reareye.R import hk.uwu.reareye.utils.RootHelper -@RequiresApi(Build.VERSION_CODES.R) abstract class BaseTileService : TileService() { companion object { private const val TAG = "BaseTileService" diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/PrefsManager.kt b/app/src/main/java/hk/uwu/reareye/ui/config/PrefsManager.kt index 76a9844..141797a 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/PrefsManager.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/PrefsManager.kt @@ -42,6 +42,12 @@ class PrefsManager(val prefs: YukiHookPrefsBridge) { return prefs.getFloat(key, defValue) } + fun getRequirementValue(key: String): Any? { + val nativePrefs = prefs.native() + if (!nativePrefs.contains(key)) return null + return nativePrefs.all()[key] + } + fun putFloat(key: String, value: Float) { prefs.edit().putFloat(key, value).apply() } @@ -50,4 +56,4 @@ class PrefsManager(val prefs: YukiHookPrefsBridge) { fun Context.getPrefsManager() = PrefsManager(this.prefs()) fun YukiHookPrefsBridge.getPrefsManager() = PrefsManager(this) } -} \ No newline at end of file +} diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt index 61f09d5..a679de4 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.graphics.BitmapFactory import android.view.View import android.view.ViewGroup +import android.webkit.WebChromeClient import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebSettings @@ -40,6 +41,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars @@ -82,6 +84,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -96,12 +99,14 @@ import hk.uwu.reareye.repository.rearstore.RearStoreInstallProgressStage import hk.uwu.reareye.repository.rearstore.RearStoreInstalledWidget import hk.uwu.reareye.repository.rearstore.RearStoreListItem import hk.uwu.reareye.repository.rearstore.RearStorePreparedInstallAsset +import hk.uwu.reareye.repository.rearstore.RearStoreQuickInstallResult import hk.uwu.reareye.repository.rearstore.RearStoreRelease import hk.uwu.reareye.repository.rearstore.RearStoreReleaseAsset import hk.uwu.reareye.repository.rearstore.RearStoreRepository import hk.uwu.reareye.repository.rearstore.RearStoreWidgetDetail import hk.uwu.reareye.repository.rearstore.RearStoreWidgetInfoType import hk.uwu.reareye.repository.rearstore.RearStoreWidgetMetadataType +import hk.uwu.reareye.repository.rearstore.evaluateRequirements import hk.uwu.reareye.repository.rearstore.resolvedType import hk.uwu.reareye.repository.rearstore.supportsModuleVersion import hk.uwu.reareye.repository.rearwallpaper.RearWallpaperMetadataOptions @@ -214,6 +219,11 @@ private data class RearStorePendingInstallConflict( val conflict: RearStoreInstallConflict, ) +private data class RearStorePostInstallBrowserState( + val title: String, + val url: String, +) + private data class RearStorePendingWallpaperInstall( val preparedAsset: RearStorePreparedInstallAsset, ) @@ -344,9 +354,17 @@ private data class RearStoreInstallInfo( val hasScene: Boolean, val scenePackageName: String?, val hasWallpaper: Boolean, + val requirementsSatisfied: Boolean, + val appListPermissionGranted: Boolean, + val missingPackages: List, + val failedConfigReasons: Map, ) -private fun defaultInstallInfo(detail: RearStoreWidgetDetail): RearStoreInstallInfo { +private fun defaultInstallInfo( + context: Context, + prefsManager: hk.uwu.reareye.ui.config.PrefsManager, + detail: RearStoreWidgetDetail, +): RearStoreInstallInfo { val widgetInfoType = detail.widgetInfo.resolvedType() val metadataType = detail.displayMetadataType() val businessSetupId = detail.widgetInfo?.businessSetup?.id.normalizedOrNull() @@ -356,6 +374,7 @@ private fun defaultInstallInfo(detail: RearStoreWidgetDetail): RearStoreInstallI val scenePackageName = sceneSetup?.packageName.normalizedOrNull() val hasCard = widgetInfoType == RearStoreWidgetInfoType.WIDGET && detail.widgetInfo?.cardSetup != null + val requirementsResult = detail.widgetInfo.evaluateRequirements(context, prefsManager) return RearStoreInstallInfo( widgetInfoType = widgetInfoType, hasComponent = widgetInfoType == RearStoreWidgetInfoType.WIDGET, @@ -368,6 +387,10 @@ private fun defaultInstallInfo(detail: RearStoreWidgetDetail): RearStoreInstallI scenePackageName = scenePackageName, hasWallpaper = widgetInfoType == RearStoreWidgetInfoType.WALLPAPER || metadataType == RearStoreWidgetMetadataType.WALLPAPER, + requirementsSatisfied = requirementsResult.satisfied, + appListPermissionGranted = requirementsResult.appListPermissionGranted, + missingPackages = requirementsResult.missingPackages, + failedConfigReasons = requirementsResult.failedConfigReasons, ) } @@ -461,6 +484,7 @@ private fun RearStoreWidgetDetail.displayMetadataType(): RearStoreWidgetMetadata private fun resolveInstallBlockMessage( context: Context, + prefsManager: hk.uwu.reareye.ui.config.PrefsManager, detail: RearStoreWidgetDetail, ): String? { val widgetInfoType = detail.widgetInfo.resolvedType() @@ -478,6 +502,28 @@ private fun resolveInstallBlockMessage( ) { return context.getString(R.string.rear_store_install_missing_business_setup) } + val requirementsResult = detail.widgetInfo.evaluateRequirements(context, prefsManager) + if (!requirementsResult.satisfied) { + return buildString { + appendLine(context.getString(R.string.rear_store_install_requirements_not_met)) + if (!requirementsResult.appListPermissionGranted && requirementsResult.missingPackages.isNotEmpty()) { + appendLine("- ${context.getString(R.string.rear_store_install_requirements_packages_permission)}") + } + requirementsResult.missingPackages.forEach { pkg -> + appendLine( + "- ${ + context.getString( + R.string.rear_store_install_requirements_missing_package_item, + pkg + ) + }" + ) + } + requirementsResult.failedConfigReasons.values.forEach { reason -> + appendLine("- $reason") + } + }.trim() + } return null } @@ -553,6 +599,55 @@ private fun parseIsoEpochMillis(value: String?): Long { return Long.MIN_VALUE } +private fun buildPostInstallUri( + detail: RearStoreWidgetDetail, + installResult: RearStoreQuickInstallResult, +): String? { + val template = detail.widgetInfo?.postInstall?.uri.normalizedOrNull() ?: return null + return template + .replace("{id}", detail.widgetId) + .replace("{business}", installResult.businessConfigId ?: "{business}") + .replace("{card}", installResult.cardId ?: "{card}") +} + +private fun launchPostInstallUri( + context: Context, + detail: RearStoreWidgetDetail, + installResult: RearStoreQuickInstallResult, + onOpenBrowser: (RearStorePostInstallBrowserState) -> Unit, +) { + val uriString = buildPostInstallUri(detail, installResult) ?: return + val uri = runCatching { uriString.toUri() }.getOrNull() ?: return + val scheme = uri.scheme?.lowercase(Locale.ROOT).orEmpty() + if (scheme == "content") { + runCatching { + context.startActivity( + Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + ) + } + return + } + if (scheme == "http" || scheme == "https") { + onOpenBrowser( + RearStorePostInstallBrowserState( + title = detail.name.normalizedOrNull() ?: detail.widgetId, + url = uriString, + ) + ) + return + } + runCatching { + context.startActivity( + Intent(Intent.ACTION_VIEW, uri).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + } +} + private fun RearStoreListItem.matchesLocalQuery(query: String): Boolean { val tokens = query.trim() .split(Regex("\\s+")) @@ -1021,6 +1116,10 @@ private fun RearStoreDetailContent( var wallpaperMetadataDraft by remember(widgetId) { mutableStateOf(null) } + var blockedInstallMessage by remember(widgetId) { mutableStateOf(null) } + var postInstallBrowserState by remember(widgetId) { + mutableStateOf(null) + } suspend fun reloadDetail() { loading = true @@ -1104,6 +1203,12 @@ private fun RearStoreDetailContent( delay(360) activeInstall = null onInstalled() + launchPostInstallUri( + context = context, + detail = widgetDetail, + installResult = installResult, + onOpenBrowser = { postInstallBrowserState = it }, + ) Toast.makeText( context, context.getString( @@ -1190,9 +1295,9 @@ private fun RearStoreDetailContent( forceOverwrite: Boolean = false, ) { val widgetDetail = detail ?: return - val blockedMessage = resolveInstallBlockMessage(context, widgetDetail) + val blockedMessage = resolveInstallBlockMessage(context, prefsManager, widgetDetail) if (blockedMessage != null) { - Toast.makeText(context, blockedMessage, Toast.LENGTH_SHORT).show() + blockedInstallMessage = blockedMessage return } val conflict = withContext(Dispatchers.IO) { @@ -1392,6 +1497,7 @@ private fun RearStoreDetailContent( selectedTab = selectedTab, widgetDetail = widgetDetail, installedWidget = installedWidget, + prefsManager = prefsManager, visibleReleases = visibleReleases, showAllReleases = showAllReleases, readmeLoading = readmeLoading, @@ -1445,6 +1551,56 @@ private fun RearStoreDetailContent( } } + OverlayDialog( + show = blockedInstallMessage != null, + title = stringResource(R.string.rear_store_install_blocked_title), + onDismissRequest = { blockedInstallMessage = null }, + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Card(modifier = Modifier.fillMaxWidth()) { + RearStoreDialogMessageCard(message = blockedInstallMessage.orEmpty()) + } + Button( + onClick = { blockedInstallMessage = null }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_cancel)) + } + } + } + + val postInstallBrowser = postInstallBrowserState + OverlayDialog( + show = postInstallBrowser != null, + title = postInstallBrowser?.title + ?: stringResource(R.string.rear_store_postinstall_browser_title), + onDismissRequest = { postInstallBrowserState = null }, + outsideMargin = DpSize(width = 10.dp, height = 10.dp), + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Card(modifier = Modifier.fillMaxWidth()) { + RearStorePostInstallBrowser( + url = postInstallBrowser?.url.orEmpty(), + ) + } + Button( + onClick = { + val targetUrl = postInstallBrowser?.url ?: return@Button + context.startActivity(Intent(Intent.ACTION_VIEW, targetUrl.toUri())) + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_store_postinstall_open_in_browser)) + } + Button( + onClick = { postInstallBrowserState = null }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_cancel)) + } + } + } + val conflictDialog = installConflict OverlayDialog( show = conflictDialog != null, @@ -1594,6 +1750,7 @@ private fun RearStoreDetailTabContent( selectedTab: RearStoreDetailTab, widgetDetail: RearStoreWidgetDetail, installedWidget: RearStoreInstalledWidget?, + prefsManager: hk.uwu.reareye.ui.config.PrefsManager, visibleReleases: List, showAllReleases: Boolean, readmeLoading: Boolean, @@ -1661,7 +1818,10 @@ private fun RearStoreDetailTabContent( onClick = onRequestUninstall, ) } - RearStoreInstallInfoCard(detail = widgetDetail) + RearStoreInstallInfoCard( + detail = widgetDetail, + prefsManager = prefsManager, + ) RearStoreRepositoryCard(detail = widgetDetail) RearStoreAuthorCard( author = widgetDetail.author, @@ -2237,8 +2397,14 @@ private fun rememberInstallInfoBadgePalette(group: RearStoreInstallInfoBadgeGrou } @Composable -private fun RearStoreInstallInfoCard(detail: RearStoreWidgetDetail) { - val installInfo = defaultInstallInfo(detail) +private fun RearStoreInstallInfoCard( + detail: RearStoreWidgetDetail, + prefsManager: hk.uwu.reareye.ui.config.PrefsManager, +) { + val context = LocalContext.current + val installInfo = remember(detail, prefsManager) { + defaultInstallInfo(context, prefsManager, detail) + } val componentPalette = rememberInstallInfoBadgePalette(RearStoreInstallInfoBadgeGroup.COMPONENT) val cardPalette = rememberInstallInfoBadgePalette(RearStoreInstallInfoBadgeGroup.CARD) val notificationPalette = @@ -2270,7 +2436,7 @@ private fun RearStoreInstallInfoCard(detail: RearStoreWidgetDetail) { palette = cardPalette, ) ) - installInfo.cardPackageName?.takeIf { installInfo.hasCard }?.let { + installInfo.cardPackageName?.let { add( RearBadgeItem( text = stringResource(R.string.rear_store_install_badge_card_package, it), @@ -2314,6 +2480,37 @@ private fun RearStoreInstallInfoCard(detail: RearStoreWidgetDetail) { ) ) } + if (!installInfo.appListPermissionGranted && installInfo.missingPackages.isNotEmpty()) { + add( + RearBadgeItem( + text = stringResource(R.string.rear_store_install_requirements_packages_permission), + palette = warningPalette, + ) + ) + } + if (installInfo.missingPackages.isNotEmpty() && installInfo.appListPermissionGranted) { + installInfo.missingPackages.forEach { pkg -> + add( + RearBadgeItem( + text = stringResource( + R.string.rear_store_install_requirements_missing_package_item, + pkg + ), + palette = warningPalette, + ) + ) + } + } + if (installInfo.failedConfigReasons.isNotEmpty()) { + installInfo.failedConfigReasons.values.forEach { reason -> + add( + RearBadgeItem( + text = reason, + palette = warningPalette, + ) + ) + } + } } Card(modifier = Modifier.fillMaxWidth()) { Column( @@ -2712,6 +2909,87 @@ private fun WebView.publishMarkdownContentHeight(onContentHeightChanged: (Int) - } } +@Composable +private fun RearStorePostInstallBrowser(url: String) { + val context = LocalContext.current + val prefsManager = remember(context) { context.getPrefsManager() } + val webViewHardwareAccelerationEnabled = prefsManager.getBoolean( + ConfigKeys.MODULE_STORE_WEBVIEW_HARDWARE_ACCELERATION, + true, + ) + val colorScheme = MiuixTheme.colorScheme + val backgroundColor = if (webViewHardwareAccelerationEnabled) { + Color.Transparent + } else { + colorScheme.surface + } + val nestedScrollInterop = rememberNestedScrollInteropConnection() + + AndroidView( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 320.dp, max = 560.dp) + .nestedScroll(nestedScrollInterop), + factory = { viewContext -> + createRearStoreBrowserWebView( + context = viewContext, + hardwareAccelerationEnabled = webViewHardwareAccelerationEnabled, + backgroundColor = backgroundColor.toArgb(), + ).apply { + loadUrl(url) + } + }, + update = { webView -> + webView.setLayerType( + if (webViewHardwareAccelerationEnabled) View.LAYER_TYPE_NONE else View.LAYER_TYPE_SOFTWARE, + null, + ) + if (webView.url != url) { + webView.loadUrl(url) + } + }, + ) +} + +@SuppressLint("SetJavaScriptEnabled") +private fun createRearStoreBrowserWebView( + context: Context, + hardwareAccelerationEnabled: Boolean, + backgroundColor: Int, +): WebView { + return ScrollWebView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setLayerType( + if (hardwareAccelerationEnabled) View.LAYER_TYPE_NONE else View.LAYER_TYPE_SOFTWARE, + null, + ) + setBackgroundColor(backgroundColor) + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + builtInZoomControls = false + displayZoomControls = false + mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + allowContentAccess = true + allowFileAccess = false + setSupportMultipleWindows(false) + userAgentString = "$userAgentString REAREyeStoreWebView" + } + webChromeClient = WebChromeClient() + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest, + ): Boolean { + return false + } + } + } +} + @Composable private fun RearStoreLoadingCard() { Card(modifier = Modifier.fillMaxWidth()) { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 307dcc6..3722a31 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -343,6 +343,26 @@ 当前版本暂不支持%1$s安装模式 当前模块版本不支持安装该组件 缺失组件配置文件 + 组件要求未满足 + 校验组件依赖应用需要已授予应用列表权限 + 缺少应用:%1$s + 无法安装 + 未知配置:%1$s + 请先设置%1$s + 请打开%1$s + 请关闭%1$s + %1$s不应处于开启状态 + %1$s不应处于关闭状态 + %1$s应不小于%2$s + %1$s应不大于%2$s + %1$s应为%2$s + %1$s不应为%2$s + %1$s应大于%2$s + %1$s应小于%2$s + %1$s应包含%2$s + %1$s不应包含%2$s + %1$s应为%2$s + %1$s不应为%2$s 安装提示 组件 %1$s 已经安装,是否继续覆盖当前安装? 组件 %1$s 已存在,是否继续覆盖当前注册? @@ -361,6 +381,8 @@ 下载中 %1$d%% 安装中 + 安装后操作 + 使用浏览器打开 组件 壁纸 卡片 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 198cf06..7360034 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -343,6 +343,26 @@ Widget: %2$s · Priority: %3$d Current version does not support %1$s install mode Current module version does not support installing this widget Missing widget config file + Widget requirements are not satisfied + Installed apps permission is required to verify widget package requirements + Missing app: %1$s + Unable to Install + Unknown config: %1$s + Please set %1$s + Turn on %1$s + Turn off %1$s + %1$s should not be turned on + %1$s should not be turned off + %1$s should be at least %2$s + %1$s should be at most %2$s + %1$s should be %2$s + %1$s should not be %2$s + %1$s should be greater than %2$s + %1$s should be less than %2$s + %1$s should contain %2$s + %1$s should not contain %2$s + %1$s should be %2$s + %1$s should not be %2$s Install Warning Widget %1$s is already installed. Continue to overwrite the current installation? Widget %1$s already exists. Continue to overwrite the current registration? @@ -361,6 +381,8 @@ Widget: %2$s · Priority: %3$d Downloading %1$d%% Installing + Post-Install + Open in Browser Widget Wallpaper Card From 537ba68718aeafb0a072aca55c5cd66241b1910f Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Wed, 6 May 2026 21:34:17 +0800 Subject: [PATCH 15/27] fix: config category favourites will disappear after version change --- .../hk/uwu/reareye/ui/screen/ConfigScreen.kt | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt index 1372935..645bd68 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt @@ -1,5 +1,6 @@ package hk.uwu.reareye.ui.screen +import android.annotation.SuppressLint import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.FastOutLinearInEasing @@ -130,6 +131,7 @@ private fun ConfigRoute.isOverlayRoute(): Boolean { this is ConfigRoute.BusinessExtraManager } +@SuppressLint("LocalContextResourcesRead") @Composable fun ConfigScreen( bottomInnerPadding: Dp = 0.dp, @@ -155,7 +157,10 @@ fun ConfigScreen( val routeScope = rememberCoroutineScope() val favoriteNodeIndex = remember(REAREyeConfig) { - buildFavoriteConfigNodeIndex(REAREyeConfig) + buildFavoriteConfigNodeIndex( + nodes = REAREyeConfig, + resolveResourceEntryName = context.resources::getResourceEntryName, + ) } val availableFavoriteNodeIds = remember(favoriteNodeIndex.entries) { favoriteNodeIndex.entries.mapTo(mutableSetOf()) { it.id } @@ -166,7 +171,9 @@ fun ConfigScreen( val initialFavoriteNodeIds = remember(storedFavoriteNodeIds, availableFavoriteNodeIds) { storedFavoriteNodeIds.filterTo(mutableSetOf()) { it in availableFavoriteNodeIds } } - var favoriteNodeIds by remember { mutableStateOf(initialFavoriteNodeIds.toSet()) } + var favoriteNodeIds by remember { + mutableStateOf(initialFavoriteNodeIds.toSet()) + } val favoriteNodes = remember(favoriteNodeIds, favoriteNodeIndex.entries) { favoriteNodeIndex.entries .filter { favoriteNodeIds.contains(it.id) } @@ -763,7 +770,7 @@ private fun Modifier.configNodeLongPress( ): Modifier { if (!enabled || isScrolling) return this - return this.pointerInput(onLongPress, isScrolling) { + return this.pointerInput(onLongPress, false) { awaitEachGesture { val down = awaitFirstDown( requireUnconsumed = false, @@ -803,14 +810,17 @@ private fun Modifier.configNodeLongPress( } } -private fun buildFavoriteConfigNodeIndex(nodes: List): FavoriteConfigNodeIndex { +private fun buildFavoriteConfigNodeIndex( + nodes: List, + resolveResourceEntryName: (Int) -> String, +): FavoriteConfigNodeIndex { val entries = mutableListOf() val nodeIdLookup = IdentityHashMap() val idCounters = mutableMapOf() - fun nextNodeId(baseId: String): String { - val current = (idCounters[baseId] ?: 0) + 1 - idCounters[baseId] = current + fun nextNodeId(counters: MutableMap, baseId: String): String { + val current = (counters[baseId] ?: 0) + 1 + counters[baseId] = current return if (current == 1) { baseId } else { @@ -818,16 +828,21 @@ private fun buildFavoriteConfigNodeIndex(nodes: List): FavoriteConfi } } - fun walk(currentNodes: List, categoryPath: List) { + fun registerNode(node: ConfigNode, nodeId: String) { + entries += FavoriteConfigNodeEntry(id = nodeId, node = node) + nodeIdLookup[node] = nodeId + } + + fun walk(currentNodes: List, categoryPath: List) { currentNodes.forEach { node -> when (node) { is ConfigCategory -> { - val baseNodeId = node.key?.let { "category:key:$it" } - ?: "category:path:${(categoryPath + node.titleRes).joinToString("/")}" - val nodeId = nextNodeId(baseNodeId) - entries += FavoriteConfigNodeEntry(id = nodeId, node = node) - nodeIdLookup[node] = nodeId - walk(node.children, categoryPath + node.titleRes) + val pathToken = node.key?.let { "key:$it" } + ?: "title:${resolveResourceEntryName(node.titleRes)}" + val baseId = "category:path:${(categoryPath + pathToken).joinToString("/")}" + val nodeId = nextNodeId(idCounters, baseId) + registerNode(node, nodeId) + walk(node.children, categoryPath + pathToken) } is ConfigGroup -> { @@ -835,14 +850,16 @@ private fun buildFavoriteConfigNodeIndex(nodes: List): FavoriteConfi } is ConfigItem -> { - val nodeId = nextNodeId("item:key:${node.key}") - entries += FavoriteConfigNodeEntry(id = nodeId, node = node) - nodeIdLookup[node] = nodeId + val nodeId = nextNodeId(idCounters, "item:key:${node.key}") + registerNode(node, nodeId) } } } } walk(nodes, emptyList()) - return FavoriteConfigNodeIndex(entries = entries, nodeIdLookup = nodeIdLookup) + return FavoriteConfigNodeIndex( + entries = entries, + nodeIdLookup = nodeIdLookup, + ) } From e97dd85697eeddf304d4b9b89941def54022ef12 Mon Sep 17 00:00:00 2001 From: ghhccghk <2137610394@qq.com> Date: Thu, 7 May 2026 22:20:29 +0800 Subject: [PATCH 16/27] feat: implement interactive license viewing and enhance LibraryItem UI - **feat**: Added an interactive `OverlayDialog` to display license details, featuring automatic URL detection and clickable links within the license text. - **ui**: Replaced static license text in `AboutScreen` with clickable `RearBadgePill` components for a more modern interface. - **ui**: Refined `LibraryItem` layout with a divider and improved alignment for developer and license information. - **refactor**: Updated `LibraryItem` to accept a license map and managed dialog visibility state within the component. - **refactor**: Added `modifier` parameter support to `RearBadgePill` to allow custom interactions and styling from parent layouts. - **refactor**: Simplified `LibraryDevelopersText` by removing redundant "Developer:" prefixes. Signed-off-by: ghhccghk <2137610394@qq.com> --- .../hk/uwu/reareye/ui/components/RearBadge.kt | 2 + .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 2 +- .../utils/other/AboutLibrariesTools.kt | 109 ++++++++++++++++-- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt b/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt index e752e3d..f10bb55 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt @@ -105,6 +105,7 @@ private fun rememberRearDefaultBadgePalette(emphasized: Boolean): RearBadgePalet fun RearBadgePill( text: String, emphasized: Boolean, + modifier: Modifier = Modifier, palette: RearBadgePalette? = null, singleLine: Boolean = true, ) { @@ -116,6 +117,7 @@ fun RearBadgePill( val resolvedPalette = palette ?: defaultPalette Surface( + modifier = modifier, color = resolvedPalette.background, shape = RoundedCornerShape(18.dp), ) { diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index a7bbd34..dd85539 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -1135,7 +1135,7 @@ private fun LicenseContent( overscrollEffect = null, ) { items(data.libraries) { - LibraryItem(it) + LibraryItem(it,data.licenses) } } diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt index 24694e4..70956dd 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt @@ -2,8 +2,23 @@ package hk.uwu.reareye.utils.other import android.content.Context import android.content.Intent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -12,14 +27,18 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import androidx.core.net.toUri import hk.uwu.reareye.R +import hk.uwu.reareye.ui.components.OverlayDialog +import hk.uwu.reareye.ui.components.RearBadgePill import hk.uwu.reareye.ui.components.card.SuperCard import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonColors import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.HorizontalDivider import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.icon.MiuixIcons @@ -78,9 +97,10 @@ fun loadLibraries(context: Context): AboutLibraries { } @Composable -fun LibraryItem(lib: Library) { +fun LibraryItem(lib: Library, licenses: Map) { val context = LocalContext.current val hasLink = lib.website != null + var showLicense by remember { mutableStateOf(null) } Card(modifier = Modifier.fillMaxWidth()) { SuperCard( @@ -115,12 +135,89 @@ fun LibraryItem(lib: Library) { } }, bottomAction = { - if (lib.developers.isNotEmpty()) { - LibraryDevelopersText(lib.developers) + if (lib.developers.isNotEmpty() || lib.licenses.isNotEmpty()) { + HorizontalDivider( + modifier = Modifier + .height(16.dp) + , + thickness = 1.dp, + color = MiuixTheme.colorScheme.outline.copy(alpha = 0.5f) + ) } - if (lib.licenses.isNotEmpty()) { - Text(text = "License: " + lib.licenses.joinToString(", ")) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + if (lib.developers.isNotEmpty()) { + Box(modifier = Modifier.weight(1f)) { + LibraryDevelopersText(lib.developers) + } + } + if (lib.licenses.isNotEmpty()) { + Column ( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + lib.licenses.forEach { licenseId -> + val license = licenses[licenseId] + RearBadgePill( + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + license?.let { showLicense = it } + }, + text = license?.name?.take(15) ?: if (licenseId.length > 15) licenseId.take(15) + "\u2026" else licenseId, + emphasized = false + ) + } + } + } + } + showLicense?.let { lic -> + val primaryColor = MiuixTheme.colorScheme.primary + OverlayDialog( + show = true, + title = lic.name, + onDismissRequest = { showLicense = null } + ) { + // 准备原始文本 + val rawText = lic.content ?: lic.url + + // 自动识别链接并构建 AnnotatedString + val annotatedText = remember(rawText) { + buildAnnotatedString { + append(rawText) + // 匹配 http/https 链接的正则表达式 + val urlPattern = Regex("(https?://[\\w-]+(\\.[\\w-]+)+(/\\S*)?)") + urlPattern.findAll(rawText).forEach { match -> + addLink( + LinkAnnotation.Url( + url = match.value, + styles = TextLinkStyles( + SpanStyle( + color = primaryColor , + fontWeight = FontWeight.Bold + ) + ) + ), + start = match.range.first, + end = match.range.last + 1 + ) + } + } + } + + Text( + modifier = Modifier + .padding(vertical = 8.dp) + .verticalScroll(rememberScrollState()), + text = annotatedText, // 传入处理好的 AnnotatedString + color = MiuixTheme.colorScheme.onSurfaceVariantSummary + ) + } } + }, ) } @@ -143,7 +240,6 @@ private fun LibraryDevelopersText(developers: List) { val url = developer.organisationUrl if (url != null) { - append("Developer: ") val start = length append(name) addLink( @@ -160,7 +256,6 @@ private fun LibraryDevelopersText(developers: List) { end = length, ) } else { - append("Developer: ") append(name) } From e43b33d548159058a7c42d42d727114ccf9cb819 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Sat, 9 May 2026 10:04:07 +0800 Subject: [PATCH 17/27] feat: custom app bounds manager & change logo design --- .../reareye/hook/scopes/system/SystemScope.kt | 6 +- .../modules/CustomBoundsCompatModule.kt | 276 +++++++ .../bounds/CustomBoundsCompatConfigCodec.kt | 159 ++++ .../config/CustomBoundsCompatManagerScreen.kt | 683 ++++++++++++++++++ .../java/hk/uwu/reareye/ui/config/Config.kt | 1 + .../hk/uwu/reareye/ui/config/ModuleConfig.kt | 9 + .../hk/uwu/reareye/ui/screen/ConfigScreen.kt | 22 +- app/src/main/res/drawable/ic_launcher.png | Bin 11990 -> 11990 bytes app/src/main/res/values-zh-rCN/strings.xml | 54 ++ app/src/main/res/values/strings.xml | 54 ++ 10 files changed, 1261 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt create mode 100644 app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt create mode 100644 app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt index 4a5aaea..51a583c 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/SystemScope.kt @@ -4,6 +4,7 @@ import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker import com.highcapable.yukihookapi.hook.log.YLog import hk.uwu.reareye.hook.scopes.Scope import hk.uwu.reareye.hook.scopes.system.modules.BackgroundWhitelistModule +import hk.uwu.reareye.hook.scopes.system.modules.CustomBoundsCompatModule import hk.uwu.reareye.hook.scopes.system.modules.DisableRearScreenCoverHook import hk.uwu.reareye.hook.scopes.system.modules.DisableSubScreenDoubleTapSleepHook import hk.uwu.reareye.hook.scopes.system.modules.DisableSubScreenDoubleTapWakeHook @@ -15,6 +16,7 @@ class SystemScope : Scope { override val hooks: List = buildList { add(GMSUnlockModule()) + add(CustomBoundsCompatModule()) if (isRearDevice) { addAll( listOf( @@ -23,11 +25,11 @@ class SystemScope : Scope { DisableRearScreenCoverHook(), DisableSubScreenDoubleTapSleepHook(), DisableSubScreenDoubleTapWakeHook(), - DisableSubScreenHighLoadModeHook() + DisableSubScreenHighLoadModeHook(), ) ) } else { YLog.debug("This device is not support rear screen, skip load some features that this device is not supported") } } -} \ No newline at end of file +} diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt new file mode 100644 index 0000000..e47794f --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt @@ -0,0 +1,276 @@ +package hk.uwu.reareye.hook.scopes.system.modules + +import android.content.res.Configuration +import android.graphics.Point +import android.graphics.Rect +import com.highcapable.kavaref.KavaRef.Companion.asResolver +import com.highcapable.kavaref.KavaRef.Companion.resolve +import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker +import com.highcapable.yukihookapi.hook.log.YLog +import hk.uwu.reareye.repository.bounds.CustomBoundsCompatAppConfig +import hk.uwu.reareye.repository.bounds.CustomBoundsCompatConfigCodec +import hk.uwu.reareye.repository.bounds.CustomBoundsMode +import hk.uwu.reareye.ui.config.ConfigKeys +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +class CustomBoundsCompatModule : YukiBaseHooker() { + override fun onHook() { + loadSystem { + val activityRecordImplClass = runCatching { + "com.android.server.wm.ActivityRecordImpl".toClass().resolve() + }.getOrElse { + YLog.warn("ActivityRecordImpl not found, skip custom rear bounds hook") + return@loadSystem + } + + activityRecordImplClass.firstMethod { + name = "resolveOverrideConfiguration" + parameterCount = 2 + }.hook().after { + val moreDebug = prefs.getBoolean(ConfigKeys.MORE_DEBUG, false) + val parentConfig = args(0).cast() ?: return@after + val resolvedConfig = args(1).cast() ?: return@after + val activityRecord = instance.field("mAr") ?: run { + if (moreDebug) YLog.debug("[$TAG] skip reason=no_activity_record") + return@after + } + val packageName = activityRecord.field("packageName") ?: run { + if (moreDebug) YLog.debug("[$TAG] skip reason=no_package") + return@after + } + val config = CustomBoundsCompatHookConfig.find( + raw = prefs.getString( + ConfigKeys.CUSTOM_BOUNDS_COMPAT_CONFIG_DATA, + CustomBoundsCompatConfigCodec.EMPTY_ARRAY, + ), + packageName = packageName, + ) ?: run { + if (moreDebug) YLog.debug("[$TAG] skip package=$packageName reason=no_config") + return@after + } + if (!config.enabled) { + if (moreDebug) YLog.debug("[$TAG] skip package=$packageName reason=disabled") + return@after + } + + val displayId = activityRecord.call("getDisplayId") ?: -1 + if (displayId != TARGET_DISPLAY_ID) { + if (moreDebug) YLog.debug("[$TAG] skip package=$packageName reason=display_id displayId=$displayId") + return@after + } + if (activityRecord.call("inMultiWindowMode") == true) { + if (moreDebug) YLog.debug("[$TAG] skip package=$packageName reason=multi_window") + return@after + } + + val parentBounds = getWindowConfigurationBounds(parentConfig) + if (parentBounds == null || parentBounds.isEmpty) { + if (moreDebug) YLog.debug("[$TAG] skip package=$packageName reason=empty_parent_bounds bounds=$parentBounds") + return@after + } + + val compatBounds = when (config.mode) { + CustomBoundsMode.EXACT_INSETS -> computeInsetsBounds(parentBounds, config) + CustomBoundsMode.CUSTOM_RATIO -> computeCompatBounds( + parentBounds = parentBounds, + aspectRatio = config.aspectRatio, + gravity = config.gravity, + scale = config.scale, + ) + + CustomBoundsMode.AUTO_RATIO -> computeCompatBounds( + parentBounds = parentBounds, + aspectRatio = CustomBoundsCompatConfigCodec.defaultAutoRatio( + parentBounds.width(), + parentBounds.height(), + ), + gravity = config.gravity, + scale = config.scale, + ) + } + applyResolvedConfiguration( + config = resolvedConfig, + bounds = compatBounds, + densityDpi = config.densityDpi.takeIf { it > 0 } ?: parentConfig.densityDpi, + rotation = config.rotationDegrees, + ) + if (moreDebug) { + YLog.debug( + "[$TAG] apply package=$packageName displayId=$displayId parent=$parentBounds bounds=$compatBounds mode=${config.mode} ratio=${config.aspectRatio} insets=${config.insetLeft},${config.insetTop},${config.insetRight},${config.insetBottom} gravity=${config.gravity} scale=${config.scale} dpi=${config.densityDpi} rotation=${config.rotationDegrees}" + ) + } + } + } + } + + private fun computeInsetsBounds(parentBounds: Rect, config: CustomBoundsCompatAppConfig): Rect { + val left = parentBounds.left + config.insetLeft + val top = parentBounds.top + config.insetTop + val right = (parentBounds.right - config.insetRight).coerceAtLeast(left + 1) + val bottom = (parentBounds.bottom - config.insetBottom).coerceAtLeast(top + 1) + return Rect(left, top, right, bottom) + } + + private fun computeCompatBounds( + parentBounds: Rect, + aspectRatio: Float, + gravity: Int, + scale: Float, + ): Rect { + val displaySize = Point(parentBounds.width(), parentBounds.height()) + val width = displaySize.x + val height = displaySize.y + val containerRatio = if (height > 0) width.toFloat() / height else 1f + val targetRatio = aspectRatio.takeIf { it > 0f } ?: 1f + + val unscaledWidth: Int + val unscaledHeight: Int + if (targetRatio >= containerRatio) { + unscaledWidth = width + unscaledHeight = (width / targetRatio).roundToInt().coerceAtLeast(1) + } else { + unscaledHeight = height + unscaledWidth = (height * targetRatio).roundToInt().coerceAtLeast(1) + } + + val scaledWidth = (unscaledWidth * scale).roundToInt().coerceIn(1, width) + val scaledHeight = (unscaledHeight * scale).roundToInt().coerceIn(1, height) + val left = when (gravity and HORIZONTAL_GRAVITY_MASK) { + LEFT -> 0 + RIGHT -> width - scaledWidth + else -> ((width - scaledWidth) / 2f).roundToInt() + } + val top = when (gravity and VERTICAL_GRAVITY_MASK) { + TOP -> 0 + BOTTOM -> height - scaledHeight + else -> ((height - scaledHeight) / 2f).roundToInt() + } + return Rect( + parentBounds.left + left, + parentBounds.top + top, + parentBounds.left + left + scaledWidth, + parentBounds.top + top + scaledHeight, + ) + } + + private fun applyResolvedConfiguration( + config: Configuration, + bounds: Rect, + densityDpi: Int, + rotation: Int, + ) { + config.densityDpi = densityDpi + val windowConfiguration = config.asResolver() + .firstField { name = "windowConfiguration" } + .get() ?: return + windowConfiguration.call("setBounds", bounds) + windowConfiguration.call("setAppBounds", bounds) + windowConfiguration.call("setMaxBounds", bounds) + if (rotation != CustomBoundsCompatConfigCodec.ROTATION_FOLLOW_SYSTEM) { + val surfaceRotation = rotation.toSurfaceRotation() + windowConfiguration.call("setRotation", surfaceRotation) + windowConfiguration.call("setDisplayRotation", surfaceRotation) + } + updateScreenDp(config, bounds) + } + + private fun updateScreenDp(config: Configuration, bounds: Rect) { + if (bounds.isEmpty || config.densityDpi <= 0) return + + val density = config.densityDpi / 160f + val widthDp = (bounds.width() / density).roundToInt() + val heightDp = (bounds.height() / density).roundToInt() + config.setHiddenIntField("compatScreenWidthDp", widthDp) + config.screenWidthDp = widthDp + config.setHiddenIntField("compatScreenHeightDp", heightDp) + config.screenHeightDp = heightDp + config.orientation = if (bounds.width() <= bounds.height()) { + Configuration.ORIENTATION_PORTRAIT + } else { + Configuration.ORIENTATION_LANDSCAPE + } + val shortSizeDp = min(widthDp, heightDp) + val longSizeDp = max(widthDp, heightDp) + config.setHiddenIntField("compatSmallestScreenWidthDp", shortSizeDp) + config.smallestScreenWidthDp = shortSizeDp + reduceScreenLayout(config.screenLayout, longSizeDp, shortSizeDp)?.let { + config.screenLayout = it + } + } + + private fun Configuration.setHiddenIntField(name: String, value: Int) { + runCatching { + asResolver().firstField { this.name = name }.set(value) + } + } + + private fun reduceScreenLayout(screenLayout: Int, longSizeDp: Int, shortSizeDp: Int): Int? { + return runCatching { + val resolver = Configuration::class.java.resolve() + val reset = resolver.firstMethod { + name = "resetScreenLayout" + parameterCount = 1 + }.invoke(screenLayout) + resolver.firstMethod { + name = "reduceScreenLayout" + parameterCount = 3 + }.invoke(reset, longSizeDp, shortSizeDp) + }.getOrNull() + } + + private fun getWindowConfigurationBounds(config: Configuration): Rect? = runCatching { + config.asResolver() + .firstField { name = "windowConfiguration" } + .get() + ?.call("getBounds") + }.getOrNull() + + private fun Int.toSurfaceRotation(): Int = when (this) { + 90 -> 1 + 180 -> 2 + 270 -> 3 + else -> 0 + } + + private fun Any?.field(name: String): T? = runCatching { + this?.asResolver()?.firstField { this.name = name }?.get() + }.getOrNull() + + private fun Any?.call(name: String, vararg args: Any?): T? = runCatching { + this?.asResolver()?.firstMethod { + this.name = name + parameterCount = args.size + }?.invoke(*args) + }.getOrNull() + + private object CustomBoundsCompatHookConfig { + @Volatile + private var lastRaw: String? = null + + @Volatile + private var lastConfigs: Map = emptyMap() + + fun find(raw: String, packageName: String?): CustomBoundsCompatAppConfig? { + if (packageName.isNullOrBlank()) return null + if (raw != lastRaw) { + lastConfigs = CustomBoundsCompatConfigCodec.parse(raw) + .associateBy { it.packageName } + lastRaw = raw + } + return lastConfigs[packageName] + } + } + + private companion object { + const val TAG = "CustomRearBounds" + const val TARGET_DISPLAY_ID = 1 + const val HORIZONTAL_GRAVITY_MASK = 7 + const val VERTICAL_GRAVITY_MASK = 112 + const val LEFT = 3 + const val RIGHT = 5 + const val TOP = 48 + const val BOTTOM = 80 + } +} diff --git a/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt b/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt new file mode 100644 index 0000000..2bb4dc1 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt @@ -0,0 +1,159 @@ +package hk.uwu.reareye.repository.bounds + +import org.json.JSONArray +import org.json.JSONObject + +data class CustomBoundsCompatAppConfig( + val packageName: String, + val enabled: Boolean = true, + val mode: CustomBoundsMode = CustomBoundsMode.AUTO_RATIO, + val aspectRatio: Float = 0f, + val gravity: Int = CustomBoundsCompatConfigCodec.DEFAULT_GRAVITY, + val scale: Float = CustomBoundsCompatConfigCodec.DEFAULT_SCALE, + val densityDpi: Int = 0, + val rotationDegrees: Int = CustomBoundsCompatConfigCodec.ROTATION_FOLLOW_SYSTEM, + val insetLeft: Int = 0, + val insetTop: Int = 0, + val insetRight: Int = 0, + val insetBottom: Int = 0, +) + +enum class CustomBoundsMode { + AUTO_RATIO, + CUSTOM_RATIO, + EXACT_INSETS; + + companion object { + fun fromString(value: String?): CustomBoundsMode? { + return when (value?.trim()?.lowercase()) { + "auto_ratio" -> AUTO_RATIO + "custom_ratio" -> CUSTOM_RATIO + "exact_insets" -> EXACT_INSETS + else -> null + } + } + } +} + +object CustomBoundsCompatConfigCodec { + const val EMPTY_ARRAY = "[]" + const val DEFAULT_GRAVITY = 17 + const val DEFAULT_SCALE = 1.0f + const val ROTATION_FOLLOW_SYSTEM = -1 + + private val supportedRotations = setOf( + ROTATION_FOLLOW_SYSTEM, + 0, + 90, + 180, + 270, + ) + + fun parse(raw: String?): List { + if (raw.isNullOrBlank()) return emptyList() + val arr = runCatching { JSONArray(raw) }.getOrNull() ?: return emptyList() + val out = mutableListOf() + val seenPackages = HashSet() + for (i in 0 until arr.length()) { + val obj = arr.optJSONObject(i) ?: continue + val packageName = obj.optString("packageName").trim() + if (packageName.isBlank() || !seenPackages.add(packageName)) continue + val aspectRatio = obj.optDouble("aspectRatio", 0.0) + .toFloat() + .takeIf { it > 0f } + ?: 0f + val parsedMode = CustomBoundsMode.fromString( + if (obj.has("mode")) obj.optString("mode") else null + ) + val mode = parsedMode ?: when { + obj.optInt("insetLeft", 0) != 0 || obj.optInt("insetTop", 0) != 0 || + obj.optInt("insetRight", 0) != 0 || obj.optInt("insetBottom", 0) != 0 -> { + CustomBoundsMode.EXACT_INSETS + } + + aspectRatio > 0f -> CustomBoundsMode.CUSTOM_RATIO + else -> CustomBoundsMode.AUTO_RATIO + } + out += CustomBoundsCompatAppConfig( + packageName = packageName, + enabled = obj.optBoolean("enabled", true), + mode = mode, + aspectRatio = aspectRatio, + gravity = obj.optInt("gravity", DEFAULT_GRAVITY), + scale = obj.optDouble("scale", DEFAULT_SCALE.toDouble()) + .toFloat() + .takeIf { it > 0f } + ?: DEFAULT_SCALE, + densityDpi = obj.optInt("densityDpi", 0).coerceAtLeast(0), + rotationDegrees = normalizeRotation( + obj.optInt( + "rotationDegrees", + ROTATION_FOLLOW_SYSTEM + ) + ), + insetLeft = obj.optInt("insetLeft", 0).coerceAtLeast(0), + insetTop = obj.optInt("insetTop", 0).coerceAtLeast(0), + insetRight = obj.optInt("insetRight", 0).coerceAtLeast(0), + insetBottom = obj.optInt("insetBottom", 0).coerceAtLeast(0), + ) + } + return out + } + + fun encode(configs: List): String = + JSONArray().also { arr -> + configs + .asSequence() + .filter { it.packageName.isNotBlank() } + .distinctBy { it.packageName } + .sortedBy { it.packageName.lowercase() } + .forEach { item -> + arr.put( + JSONObject() + .put("packageName", item.packageName.trim()) + .put("enabled", item.enabled) + .put("mode", item.mode.name.lowercase()) + .put("aspectRatio", item.aspectRatio.toDouble()) + .put("gravity", item.gravity) + .put("scale", item.scale.toDouble()) + .put("densityDpi", item.densityDpi) + .put("rotationDegrees", normalizeRotation(item.rotationDegrees)) + .put("insetLeft", item.insetLeft) + .put("insetTop", item.insetTop) + .put("insetRight", item.insetRight) + .put("insetBottom", item.insetBottom) + ) + } + }.toString() + + fun normalizeForPackages( + configs: List, + packageNames: Set, + ): List { + val byPackage = configs.associateBy { it.packageName } + return packageNames + .asSequence() + .map { it.trim() } + .filter { it.isNotBlank() } + .distinct() + .sortedBy { it.lowercase() } + .map { packageName -> + byPackage[packageName] ?: CustomBoundsCompatAppConfig(packageName = packageName) + } + .toList() + } + + fun defaultAutoRatio(parentWidth: Int, parentHeight: Int): Float { + val longSide = maxOf(parentWidth, parentHeight) + val shortSide = minOf(parentWidth, parentHeight) + return if (longSide > 0) shortSide.toFloat() / longSide else 1f + } + + fun normalizeRotation(rotationDegrees: Int): Int { + return if (rotationDegrees in supportedRotations) { + rotationDegrees + } else { + ROTATION_FOLLOW_SYSTEM + } + } +} diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt new file mode 100644 index 0000000..f966ac4 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt @@ -0,0 +1,683 @@ +package hk.uwu.reareye.ui.components.config + +import android.annotation.SuppressLint +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.rounded.EditNote +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import hk.uwu.reareye.R +import hk.uwu.reareye.repository.bounds.CustomBoundsCompatAppConfig +import hk.uwu.reareye.repository.bounds.CustomBoundsCompatConfigCodec +import hk.uwu.reareye.repository.bounds.CustomBoundsMode +import hk.uwu.reareye.ui.components.DialogFormColumn +import hk.uwu.reareye.ui.components.OverlayDialog +import hk.uwu.reareye.ui.components.RearBadgeItem +import hk.uwu.reareye.ui.components.card.ModuleStyleDeleteAction +import hk.uwu.reareye.ui.components.card.ModuleStyleIconAction +import hk.uwu.reareye.ui.components.card.ModuleStyleManagerCard +import hk.uwu.reareye.ui.components.card.SuperCard +import hk.uwu.reareye.ui.components.motion.ArtRevealItem +import hk.uwu.reareye.ui.components.rememberRearAccentBadgePalette +import hk.uwu.reareye.ui.config.ConfigItem +import hk.uwu.reareye.ui.config.ConfigKeys +import hk.uwu.reareye.ui.config.ConfigType +import hk.uwu.reareye.ui.config.PrefsManager +import hk.uwu.reareye.ui.theme.rearAcrylicEffect +import hk.uwu.reareye.ui.theme.rearAcrylicSource +import hk.uwu.reareye.ui.theme.rememberAcrylicHazeState +import hk.uwu.reareye.ui.theme.rememberAcrylicHazeStyle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.ButtonDefaults +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.Icon +import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior +import top.yukonga.miuix.kmp.basic.Scaffold +import top.yukonga.miuix.kmp.basic.SpinnerEntry +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextField +import top.yukonga.miuix.kmp.basic.TopAppBar +import top.yukonga.miuix.kmp.icon.MiuixIcons +import top.yukonga.miuix.kmp.icon.extended.Delete +import top.yukonga.miuix.kmp.preference.SwitchPreference +import top.yukonga.miuix.kmp.preference.WindowSpinnerPreference +import top.yukonga.miuix.kmp.theme.miuixShape +import top.yukonga.miuix.kmp.utils.overScrollVertical +import top.yukonga.miuix.kmp.utils.scrollEndHaptic +import java.util.Locale + +private data class CustomBoundsOption( + val value: Int, + @param:StringRes val titleRes: Int, +) + +private val gravityOptions = listOf( + CustomBoundsOption(17, R.string.custom_bounds_position_center), + CustomBoundsOption(48, R.string.custom_bounds_position_top), + CustomBoundsOption(80, R.string.custom_bounds_position_bottom), + CustomBoundsOption(3, R.string.custom_bounds_position_left), + CustomBoundsOption(5, R.string.custom_bounds_position_right), +) + +private val rotationOptions = listOf( + CustomBoundsOption( + CustomBoundsCompatConfigCodec.ROTATION_FOLLOW_SYSTEM, + R.string.custom_bounds_rotation_follow, + ), + CustomBoundsOption(0, R.string.custom_bounds_rotation_0), + CustomBoundsOption(90, R.string.custom_bounds_rotation_90), + CustomBoundsOption(180, R.string.custom_bounds_rotation_180), + CustomBoundsOption(270, R.string.custom_bounds_rotation_270), +) + +private val modeOptions = listOf( + CustomBoundsOption(0, R.string.custom_bounds_mode_auto_ratio), + CustomBoundsOption(1, R.string.custom_bounds_mode_custom_ratio), + CustomBoundsOption(2, R.string.custom_bounds_mode_exact_insets), +) + +@SuppressLint("LocalContextGetResourceValueCall") +@Composable +fun CustomBoundsCompatManagerScreen( + prefsManager: PrefsManager, + onBack: () -> Unit, +) { + val context = LocalContext.current + val layoutDirection = LocalLayoutDirection.current + val scrollBehavior = MiuixScrollBehavior() + val hazeState = rememberAcrylicHazeState() + val hazeStyle = rememberAcrylicHazeStyle() + rememberCoroutineScope() + val configs = remember { mutableStateListOf() } + var loaded by remember { mutableStateOf(false) } + var dataCardsVisible by remember { mutableStateOf(false) } + var showAppSelector by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(false) } + var editingPackageName by remember { mutableStateOf(null) } + var draftEnabled by remember { mutableStateOf(true) } + var draftMode by remember { mutableIntStateOf(0) } + var draftAspectRatio by remember { mutableStateOf("") } + var draftGravity by remember { mutableIntStateOf(CustomBoundsCompatConfigCodec.DEFAULT_GRAVITY) } + var draftScale by remember { mutableStateOf("") } + var draftDensityDpi by remember { mutableStateOf("") } + var draftRotationDegrees by remember { + mutableIntStateOf(CustomBoundsCompatConfigCodec.ROTATION_FOLLOW_SYSTEM) + } + var draftInsetLeft by remember { mutableStateOf("") } + var draftInsetTop by remember { mutableStateOf("") } + var draftInsetRight by remember { mutableStateOf("") } + var draftInsetBottom by remember { mutableStateOf("") } + + val appSelectorItem = remember { + ConfigItem( + key = ConfigKeys.CUSTOM_BOUNDS_COMPAT_APPS, + titleRes = R.string.custom_bounds_pick_apps, + descriptionRes = R.string.custom_bounds_pick_apps_desc, + type = ConfigType.AppList(defaultValues = emptySet()), + ) + } + + fun replaceConfigs(nextConfigs: List) { + configs.clear() + configs.addAll(nextConfigs.sortedBy { it.packageName.lowercase() }) + } + + fun loadConfigsFromPrefs(): List { + val selectedPackages = prefsManager.getStringSet( + ConfigKeys.CUSTOM_BOUNDS_COMPAT_APPS, + emptySet(), + ) + val storedConfigs = CustomBoundsCompatConfigCodec.parse( + prefsManager.getString( + ConfigKeys.CUSTOM_BOUNDS_COMPAT_CONFIG_DATA, + CustomBoundsCompatConfigCodec.EMPTY_ARRAY, + ) + ) + return CustomBoundsCompatConfigCodec.normalizeForPackages(storedConfigs, selectedPackages) + } + + fun persist(nextConfigs: List = configs.toList()) { + val packages = nextConfigs.mapTo(linkedSetOf()) { it.packageName } + val encoded = CustomBoundsCompatConfigCodec.encode(nextConfigs) + prefsManager.putStringSet(ConfigKeys.CUSTOM_BOUNDS_COMPAT_APPS, packages) + prefsManager.putString(ConfigKeys.CUSTOM_BOUNDS_COMPAT_CONFIG_DATA, encoded) + } + + fun syncAfterAppSelection() { + val nextConfigs = loadConfigsFromPrefs() + replaceConfigs(nextConfigs) + persist(nextConfigs) + } + + fun openEditDialog(item: CustomBoundsCompatAppConfig) { + editingPackageName = item.packageName + draftEnabled = item.enabled + draftMode = when (item.mode) { + CustomBoundsMode.AUTO_RATIO -> 0 + CustomBoundsMode.CUSTOM_RATIO -> 1 + CustomBoundsMode.EXACT_INSETS -> 2 + } + draftAspectRatio = if (item.aspectRatio > 0f) formatFloat(item.aspectRatio) else "" + draftGravity = normalizeGravity(item.gravity) + draftScale = formatFloat(item.scale) + draftDensityDpi = item.densityDpi.takeIf { it > 0 }?.toString().orEmpty() + draftRotationDegrees = CustomBoundsCompatConfigCodec.normalizeRotation(item.rotationDegrees) + draftInsetLeft = item.insetLeft.takeIf { it > 0 }?.toString().orEmpty() + draftInsetTop = item.insetTop.takeIf { it > 0 }?.toString().orEmpty() + draftInsetRight = item.insetRight.takeIf { it > 0 }?.toString().orEmpty() + draftInsetBottom = item.insetBottom.takeIf { it > 0 }?.toString().orEmpty() + showDialog = true + } + + fun submitDialog() { + val packageName = editingPackageName ?: return + val mode = when (draftMode) { + 1 -> CustomBoundsMode.CUSTOM_RATIO + 2 -> CustomBoundsMode.EXACT_INSETS + else -> CustomBoundsMode.AUTO_RATIO + } + val aspectRatio = draftAspectRatio.trim().toFloatOrNull() + val scale = draftScale.trim().toFloatOrNull() + val densityDpi = draftDensityDpi.trim().ifBlank { "0" }.toIntOrNull() + val gravity = draftGravity + val rotation = draftRotationDegrees + val insetLeft = draftInsetLeft.trim().ifBlank { "0" }.toIntOrNull() + val insetTop = draftInsetTop.trim().ifBlank { "0" }.toIntOrNull() + val insetRight = draftInsetRight.trim().ifBlank { "0" }.toIntOrNull() + val insetBottom = draftInsetBottom.trim().ifBlank { "0" }.toIntOrNull() + + if (scale == null || scale <= 0f || densityDpi == null || densityDpi < 0 || + gravityOptions.none { it.value == gravity } || + CustomBoundsCompatConfigCodec.normalizeRotation(rotation) != rotation || + insetLeft == null || insetLeft < 0 || insetTop == null || insetTop < 0 || + insetRight == null || insetRight < 0 || insetBottom == null || insetBottom < 0 || + (mode == CustomBoundsMode.CUSTOM_RATIO && (aspectRatio == null || aspectRatio <= 0f)) + ) { + Toast.makeText( + context, + context.getString(R.string.custom_bounds_form_invalid), + Toast.LENGTH_SHORT, + ).show() + return + } + + val nextConfig = CustomBoundsCompatAppConfig( + packageName = packageName, + enabled = draftEnabled, + mode = mode, + aspectRatio = aspectRatio ?: 0f, + gravity = gravity, + scale = scale, + densityDpi = densityDpi, + rotationDegrees = rotation, + insetLeft = insetLeft, + insetTop = insetTop, + insetRight = insetRight, + insetBottom = insetBottom, + ) + val index = configs.indexOfFirst { it.packageName == packageName } + if (index >= 0) { + configs[index] = nextConfig + } + replaceConfigs(configs.toList()) + persist() + showDialog = false + Toast.makeText( + context, + context.getString(R.string.custom_bounds_saved), + Toast.LENGTH_SHORT, + ).show() + } + + LaunchedEffect(Unit) { + delay(220) + replaceConfigs(withContext(Dispatchers.IO) { loadConfigsFromPrefs() }) + loaded = true + delay(90) + dataCardsVisible = true + } + + if (showAppSelector) { + BackHandler { showAppSelector = false } + AppListSelectorScreen( + configItem = appSelectorItem, + prefsManager = prefsManager, + onCancel = { showAppSelector = false }, + onSave = { + syncAfterAppSelection() + showAppSelector = false + }, + ) + return + } + + Scaffold( + topBar = { + TopAppBar( + modifier = Modifier.rearAcrylicEffect(hazeState, hazeStyle), + color = Color.Transparent, + title = stringResource(R.string.custom_bounds_compat_manager), + navigationIconPadding = 12.dp, + actionIconPadding = 12.dp, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + modifier = Modifier.graphicsLayer { + if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f + }, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + actions = { + IconButton(onClick = { if (loaded) showAppSelector = true }) { + Icon(imageVector = Icons.Filled.Add, contentDescription = null) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .scrollEndHaptic() + .overScrollVertical() + .rearAcrylicSource(hazeState) + .padding(horizontal = 12.dp), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding() + 12.dp, + bottom = paddingValues.calculateBottomPadding() + 12.dp, + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + overscrollEffect = null, + ) { + item { + Card( + modifier = Modifier + .padding(bottom = 12.dp) + .fillMaxWidth(), + ) { + SuperCard( + title = stringResource(R.string.custom_bounds_hint_title), + summary = stringResource(R.string.custom_bounds_hint), + onClick = {}, + bottomAction = { + Button( + onClick = { showAppSelector = true }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = null, + modifier = Modifier.padding(end = 6.dp), + ) + Text(text = stringResource(R.string.custom_bounds_pick_apps)) + } + }, + ) + } + } + + if (!dataCardsVisible) { + item { + Card( + modifier = Modifier.fillMaxWidth(), + insideMargin = PaddingValues(vertical = 24.dp), + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + InfiniteProgressIndicator() + Text(text = stringResource(R.string.rear_widget_loading_data)) + } + } + } + } + } + + if (dataCardsVisible) { + itemsIndexed( + items = configs, + key = { _, item -> item.packageName }, + contentType = { _, _ -> "custom_bounds_item" }, + ) { _, item -> + ModuleStyleManagerCard( + title = item.packageName, + summaryLines = emptyList(), + badges = customBoundsBadges(item), + onCardClick = { openEditDialog(item) }, + leftAction = { + ModuleStyleIconAction( + icon = Icons.Rounded.EditNote, + onClick = { openEditDialog(item) }, + ) + }, + rightAction = { + ModuleStyleDeleteAction( + icon = MiuixIcons.Delete, + text = stringResource(R.string.rear_widget_action_delete), + onClick = { + configs.remove(item) + persist() + }, + ) + }, + ) + } + } + + item { + if (dataCardsVisible && configs.isEmpty()) { + ArtRevealItem(visible = true, delayMillis = 40) { + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.custom_bounds_empty), + modifier = Modifier.padding(16.dp), + ) + } + } + } + } + } + } + + OverlayDialog( + show = showDialog, + title = stringResource(R.string.custom_bounds_edit_app), + onDismissRequest = { showDialog = false }, + ) { + DialogFormColumn { + TextField( + value = editingPackageName.orEmpty(), + onValueChange = {}, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.rear_widget_target_package), + enabled = false, + readOnly = true, + singleLine = true, + ) + SwitchPreference( + title = stringResource(R.string.custom_bounds_enable_app), + checked = draftEnabled, + onCheckedChange = { draftEnabled = it }, + ) + CustomBoundsDropdownPreference( + title = stringResource(R.string.custom_bounds_mode), + description = stringResource(R.string.custom_bounds_mode_help), + options = modeOptions, + selectedValue = draftMode, + onSelected = { draftMode = it }, + ) + if (draftMode == 1) { + TextField( + value = draftAspectRatio, + onValueChange = { draftAspectRatio = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.custom_bounds_aspect_ratio), + singleLine = true, + ) + } + if (draftMode == 2) { + TextField( + value = draftInsetLeft, + onValueChange = { draftInsetLeft = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.custom_bounds_inset_left), + singleLine = true, + ) + TextField( + value = draftInsetTop, + onValueChange = { draftInsetTop = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.custom_bounds_inset_top), + singleLine = true, + ) + TextField( + value = draftInsetRight, + onValueChange = { draftInsetRight = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.custom_bounds_inset_right), + singleLine = true, + ) + TextField( + value = draftInsetBottom, + onValueChange = { draftInsetBottom = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.custom_bounds_inset_bottom), + singleLine = true, + ) + } + if (draftMode == 0) { + Text( + text = stringResource(R.string.custom_bounds_auto_ratio_hint), + modifier = Modifier.fillMaxWidth(), + ) + } + CustomBoundsDropdownPreference( + title = stringResource(R.string.custom_bounds_gravity), + description = stringResource(R.string.custom_bounds_gravity_help), + options = gravityOptions, + selectedValue = draftGravity, + onSelected = { draftGravity = it }, + ) + TextField( + value = draftScale, + onValueChange = { draftScale = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.custom_bounds_scale), + singleLine = true, + ) + TextField( + value = draftDensityDpi, + onValueChange = { draftDensityDpi = it }, + modifier = Modifier.fillMaxWidth(), + label = stringResource(R.string.custom_bounds_density_dpi), + singleLine = true, + ) + CustomBoundsDropdownPreference( + title = stringResource(R.string.custom_bounds_rotation_degrees), + description = stringResource(R.string.custom_bounds_rotation_help), + options = rotationOptions, + selectedValue = draftRotationDegrees, + onSelected = { draftRotationDegrees = it }, + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { submitDialog() }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_confirm)) + } + Button( + onClick = { showDialog = false }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_cancel)) + } + } + } + } +} + +private fun formatFloat(value: Float): String = + String.format(Locale.US, "%.4f", value).trimEnd('0').trimEnd('.') + +private fun normalizeGravity(gravity: Int): Int { + return gravityOptions.firstOrNull { it.value == gravity }?.value + ?: CustomBoundsCompatConfigCodec.DEFAULT_GRAVITY +} + +@Composable +private fun CustomBoundsDropdownPreference( + title: String, + description: String, + options: List, + selectedValue: Int, + onSelected: (Int) -> Unit, +) { + val optionTitles = options.map { stringResource(it.titleRes) } + val optionEntries = remember(optionTitles) { + optionTitles.map { SpinnerEntry(title = it) } + } + val selectedIndex = options.indexOfFirst { it.value == selectedValue }.coerceAtLeast(0) + WindowSpinnerPreference( + modifier = Modifier.clip(miuixShape(16.dp)), + items = optionEntries, + selectedIndex = selectedIndex, + title = title, + summary = description, + onSelectedIndexChange = { index -> onSelected(options[index].value) }, + ) +} + +@Composable +private fun customBoundsBadges(item: CustomBoundsCompatAppConfig): List { + val statePalette = rememberRearAccentBadgePalette( + if (item.enabled) Color(0xFF10B981) else Color(0xFF64748B) + ) + val ratioPalette = rememberRearAccentBadgePalette(Color(0xFF6366F1)) + val positionPalette = rememberRearAccentBadgePalette(Color(0xFF8B5CF6)) + val densityPalette = rememberRearAccentBadgePalette(Color(0xFF0EA5E9)) + val rotationPalette = rememberRearAccentBadgePalette(Color(0xFFF59E0B)) + return listOf( + RearBadgeItem( + text = if (item.enabled) { + stringResource(R.string.custom_bounds_enabled) + } else { + stringResource(R.string.custom_bounds_disabled) + }, + emphasized = item.enabled, + palette = statePalette, + ), + RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_mode, + modeTitle(item.mode), + ), + palette = ratioPalette, + ), + if (item.mode == CustomBoundsMode.CUSTOM_RATIO) RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_ratio, + formatFloat(item.aspectRatio), + ), + palette = ratioPalette, + ) else if (item.mode == CustomBoundsMode.EXACT_INSETS) RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_insets, + "${item.insetLeft},${item.insetTop},${item.insetRight},${item.insetBottom}", + ), + palette = ratioPalette, + ) else RearBadgeItem( + text = stringResource(R.string.custom_bounds_auto_ratio_hint), + palette = ratioPalette, + ), + RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_scale, + formatFloat(item.scale), + ), + palette = ratioPalette, + ), + RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_position, + gravityTitle(item.gravity), + ), + palette = positionPalette, + ), + RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_density, + item.densityDpi.takeIf { it > 0 }?.toString() + ?: stringResource(R.string.custom_bounds_follow_system), + ), + palette = densityPalette, + ), + RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_rotation, + formatRotation(item.rotationDegrees), + ), + palette = rotationPalette, + ), + ) +} + +@Composable +private fun modeTitle(mode: CustomBoundsMode): String { + return when (mode) { + CustomBoundsMode.AUTO_RATIO -> stringResource(R.string.custom_bounds_mode_auto_ratio) + CustomBoundsMode.CUSTOM_RATIO -> stringResource(R.string.custom_bounds_mode_custom_ratio) + CustomBoundsMode.EXACT_INSETS -> stringResource(R.string.custom_bounds_mode_exact_insets) + } +} + +@Composable +private fun gravityTitle(gravity: Int): String { + val option = gravityOptions.firstOrNull { it.value == gravity } + return if (option != null) { + stringResource(option.titleRes) + } else { + stringResource(R.string.custom_bounds_position_custom, gravity) + } +} + +@Composable +private fun formatRotation(rotationDegrees: Int): String { + return if (rotationDegrees == CustomBoundsCompatConfigCodec.ROTATION_FOLLOW_SYSTEM) { + stringResource(R.string.custom_bounds_follow_system) + } else { + stringResource(R.string.custom_bounds_rotation_value, rotationDegrees) + } +} diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt b/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt index b8a26cc..1929dda 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt @@ -196,6 +196,7 @@ sealed class ConfigType { SCENE_ROUTE, CARD, BUSINESS_EXTRA, + BOUNDS, } data class Manager(val managerType: ManagerType) : ConfigType() { diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt index eae086d..950bb28 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt @@ -23,6 +23,9 @@ object ConfigKeys { const val ACTIVITIES_WHITELIST_APPS = "activities_whitelist_apps" const val ALLOW_ALL_ACTIVITIES = "allow_all_activities" const val HOOK_SKIP_LOCK_BACK_HOME = "enable_skip_lock_back_home" + const val CFG_CUSTOM_BOUNDS_COMPAT_MANAGER = "cfg_custom_bounds_compat_manager" + const val CUSTOM_BOUNDS_COMPAT_APPS = "custom_bounds_compat_apps" + const val CUSTOM_BOUNDS_COMPAT_CONFIG_DATA = "custom_bounds_compat_config_data" const val HOOK_MUSIC_CONTROLS_WHITELIST = "enable_music_controls_whitelist_hook" const val MUSIC_CONTROLS_WHITELIST_APPS = "music_controls_whitelist_apps" @@ -303,6 +306,12 @@ val REAREyeConfig = listOf( titleRes = R.string.skip_lock_back_home, type = ConfigType.BooleanVal(defaultValue = false) ), + ConfigItem( + key = ConfigKeys.CFG_CUSTOM_BOUNDS_COMPAT_MANAGER, + titleRes = R.string.custom_bounds_compat_manager, + descriptionRes = R.string.custom_bounds_compat_manager_desc, + type = ConfigType.Manager(ConfigType.ManagerType.BOUNDS), + ), ConfigItem( key = ConfigKeys.HOOK_DISABLE_REAR_SCREEN_COVER, titleRes = R.string.cfg_disable_rear_screen_cover, diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt index 645bd68..878c04c 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt @@ -53,6 +53,7 @@ import hk.uwu.reareye.ui.components.config.BusinessExtraConfigManagerScreen import hk.uwu.reareye.ui.components.config.BusinessManagerScreen import hk.uwu.reareye.ui.components.config.CardManagerScreen import hk.uwu.reareye.ui.components.config.ConfigNodeRow +import hk.uwu.reareye.ui.components.config.CustomBoundsCompatManagerScreen import hk.uwu.reareye.ui.components.config.RearWallpaperManagerScreen import hk.uwu.reareye.ui.components.config.SceneRouteManagerScreen import hk.uwu.reareye.ui.config.ConfigCategory @@ -102,6 +103,7 @@ private sealed interface ConfigRoute { data object SceneRouteManager : ConfigRoute data object CardManager : ConfigRoute data object BusinessExtraManager : ConfigRoute + data object CustomBoundsCompatManager : ConfigRoute } private const val NAV_BAR_EXIT_DURATION_MS = 220L @@ -128,7 +130,8 @@ private fun ConfigRoute.isOverlayRoute(): Boolean { this is ConfigRoute.BusinessManager || this is ConfigRoute.SceneRouteManager || this is ConfigRoute.CardManager || - this is ConfigRoute.BusinessExtraManager + this is ConfigRoute.BusinessExtraManager || + this is ConfigRoute.CustomBoundsCompatManager } @SuppressLint("LocalContextResourcesRead") @@ -356,6 +359,10 @@ fun ConfigScreen( openOverlayRoute(ConfigRoute.BusinessExtraManager) } + ConfigType.ManagerType.BOUNDS -> { + openOverlayRoute(ConfigRoute.CustomBoundsCompatManager) + } + null -> Unit } }, @@ -411,6 +418,10 @@ fun ConfigScreen( openOverlayRoute(ConfigRoute.BusinessExtraManager) } + ConfigType.ManagerType.BOUNDS -> { + openOverlayRoute(ConfigRoute.CustomBoundsCompatManager) + } + null -> Unit } }, @@ -461,6 +472,10 @@ fun ConfigScreen( openOverlayRoute(ConfigRoute.BusinessExtraManager) } + ConfigType.ManagerType.BOUNDS -> { + openOverlayRoute(ConfigRoute.CustomBoundsCompatManager) + } + null -> Unit } }, @@ -506,6 +521,11 @@ fun ConfigScreen( prefsManager = prefsManager, onBack = { closeOverlayRoute() }, ) + + ConfigRoute.CustomBoundsCompatManager -> CustomBoundsCompatManagerScreen( + prefsManager = prefsManager, + onBack = { closeOverlayRoute() }, + ) } } } diff --git a/app/src/main/res/drawable/ic_launcher.png b/app/src/main/res/drawable/ic_launcher.png index b9cb898c9e10c71e5a88ec68f959484a813b67e7..a6a4e06b1863479f972d4099b2549897008d8e4c 100644 GIT binary patch literal 11990 zcmeHt`9GBF|Nm_%N|p*mSxPEficrL;Y$3{?y~vbpDvW)GB*%VK$TF5}jct{<-qF!8f*@w_ zkr`q?2!3t(43NREL(g;`ct8-xvHcfQ-vo*ZL88zdjhiMusVftRzpV&?lN<08l`cV| zUv1Qx6PPa^o4)kaXh!!PZ(q!l6tlO(FS4F$BX3|Vh|c`!+4PEk@V4{cSMdt&?hZCQ5NjHRvWxE++qdq(i;DL z{7(=68NvV686M*8prGqln2^HsZ6$iz<|L7&M}-|yPdr?f9AN@i zxQF|JCGe|Ix^NQ%5iJ8zE&R7{&Jm(_Ge*0D3c8u6IGLfI0|$$}*W(roAI1OqoaOU2 zRXp>ot>ol7QMr_@%uyGt72G(ZkaeYdlCxoy2MU6~b$JWk@uggnvcz}=YOLkwE@op%I2M#~27;J)oE7$0+Db~ANw8Hn+J$#T-GrBC@I_-2^s&iZ z9}{#+Tp?D730lANanPjJWFXh*r)vLVT?>|Q)#AI$r@s@=nf43V7{E>M!r57Z4B4Te zFxSN`#079g@FVsOnmWM@O$F`zzgjxLGACEAZxjMI0>(vbN$pyhdayODf=7!puW1q4L22@ zXcu4!(qo4jl)aSc2B9kB6`sEKiYa33&?Qj_a%nQb!wz!zx>cLlKZPtVFhidX0!bsd zVc!PDaiLG$*ApjMWrM;W>+u5DkZh8C{Mazcn|3F#?(9`UUqZjE6uFHfd2hYdL@$?$ z63=h}N5aQmmfV(lvVy#xCVDC$J?rbnS^?1QHRRuqnd}dM%LrYbAgWI1e0T)gYhtny9J0(0xn~mBPCzf8eEgdbBcf_JM{Iu7|9EA$ zcT@0@|M^eSPEe4z#nPHgW>tdRg_bg`Nroa+c}N~=@B$X86@yCY!m~B&X-1!>m7g6H zz}p>QKjH*IGQ4EB>$J~A+(Qj1LH}-LA2GGcYl2*5$!k@GPcm$ms)}L^>T=_+aAoY_maKg2gb$+ECiA#p6t_vx+vhM>r;|PnG=MYLBo@ z8w6s~hZ>w&Aqy#RZc21oaZP^3Q4tfFI3t-PBQZ6{+P1pm=iIq?S+-+Yh4LofU3Ldb)V=AzX`JM$n(w2O7CX3(@NEXC2|WoD zvKig{+v)Rlpp$i8J?K;0y)(Fu@aK3thXxm!@-w(3<&%My*ND)b?ebtIIq=ci%Gw@qNC~!eY8W z{Kxi9fhl=u51REqFztW9IpCs;QN>0#a~!ryAyLZC(E`HMA1-GQam**RA@yz!qd6e; zuZ_d)Bll|iqFPK&s(kEOrTJ~5sphKH3Pn2CW=JkFD5+P_mNGNsXL(N2Jt|rp7CDxGrOnE0K0{&kzciC`7clr54#;=v6IRD-sfc( zlSJyX6C>y%SAnNXo$7gczk#y@%D5oDdA#16UZ$aI?7u_O(8NE*QdW(>KOb-}GeLAN zEa40m=R+k_H|VLWdQE};xUaWtx9^7V9{h0O3QJHLI0K613V0OB7@?VsJie)w|C?8E}n!c|kR=jzoAxW6Y$_?%&{byKobCf3co2K)_RB zceaO8>3@co$S_&_>X>W8I@ZDZLR8uD;_uiyr;dk@(Mbf|{`$^? zuVyVdh!zD4d!Bt6&Ze0Tpr@7nC_yvzi;1i>h;n{`|ZQYU~we?ug^)5#WpS0adxM?&hwog4>(&W%W`%6*>2Q!qaw$pz*Y zYywioJQfw~x-rmD2ozK-KeJ);r@18L^zyGv|JIhFZIYdIf+{D|hEd$@mp$>79FWc9 z4BO4EPa?`_lIy(7#Zkh&TKsu!k4St%wj)CY#t0$lbZZOQ6yXA`<{}A8OeiTtJOZruxTVplS9mml2BIBNw@RKOBr1=7A^OTk&-M#SvJ+PkP(*)%ujx`t~}kr|fO@ z#pOFd`L4$`&G!Wy4SeLc?Sv=v=I}@I5C)AjzMR_#mrDF2s&C21U(Wur{OzMjcjtpM z2QgPZS^gWIw_6%T_xOFxi?9d0#V!Uy&mqC|H4L6j8vQsY-6KS~s+w|BgYE>m=U z=6y*0&1AlG6g3Q%YVvdM^Q_foF)XVQOSs6cF?AH;v9RT%|Awu8q$>#zu>)H8hhj@bHZ=hRzeiA_%Fq>Sg$i!u`VGgP8 z)Sr0A!N)SV=l{^YG^=UYDXW_uBx|sAUOs5s{vZ^5^;zKI!E9$`hY8-5*Y&TFuVpWu zlH8+7)UFs{8xsq7MXo(*9xd!w%ESER@T6AstZgk7hjk8~ z(&+=qhlv|e%xV?^1vOkzeUzUopPyT*m41}f>b5xTEvYU8h@kMg8E%yqK5$x{)k=Tp zcb&t6gBfBLA8>Z=&v{w0VUJLi^xA~n>~Ls za#Xpumk01*7ZlK-tv8gI66@hLs64yylFE{uk)uJPXCcvh`UdeJ*`}$^!GJw-bDuW8 z3T9A)CA?M#}5}C1a=g61L6MaYD(w!crXl8Mz=k^ZC811sojH3QDwB!Eu>`U zF6!5QjN)MjWyD!idB!}5uenIl&^+wKrNu^;R^BxS{}J7l5noaS-TINVd!guMBaCUb zvO~6;eHlB%{;K8(zYfToe!1)23n~2}U9VQT(>E!mQTcR_)j8?HTr6I zpg-*C&-YV{j-B75gHtZ?@s8Hv;kP8JftS=rCPuVGF#H~G!A|GTf4wwL0GGM{7*#r1 zRepbPZ}?H(TG(#S`9YkfnThmB(t-D?fg{@_V2Zr5eEvx7!_{oy8e=Apw$Liz+*PMM zwKpvC8gU)m`Eq#Q=NfoRc})_>sm*v);MxSsXb<7uXNC~)MupS)<8mefm;)F2*r9bH zaE<+{Ke5B4#7?*iNt$m3CoQQyMwvqGB zDj&c%1C|z+BZ87oEAcNHm(}t8I8Ks==kIT(1fn>ovIm@zG*bAg78Hcj_oBZs5uR`< zbn8#8zdj+Nc4D}TsF8;^srgNR(=(MI(@sZup|WgKh#v`G9IhO3<&5VYN(uYk3v&Y#{G;z&s>)^dvEgW*4N-)!O}Z5SJO>0RSWf2vPfoTi#ziY_PQIKdoyRT|-xsW@$jMLc8Y`Z4~?5r39DSmQW!>d)~_Q z0F|sEg*7_XhZ3!1jgX(Y=0dMQhXA@q#p~=^A!a$E>~V_R6#-q z!$|#>{C+H#nhIm63>BCX#!mmGMc5`4EJVePQKxgxg5_tR2BngSB!M7zD|1fWxi!LH z45zY%r-ZSnJfkHtMSdhT4zgc_Ll%dap@5^? zeierdlUofualZaao^5|na5~$R0kCK5eczsNe@{2u=_bD@=}!Gd4x4@7)#0{D9g%u0 z3AqZL@+guqCcej}ADTh}fz+}10iiODPb>`}lA|6@y_@UC`4dGLjj_5P= z`c@mmY+Tw0l$;jf%#}jS&<*&9#j2wsN2qc6u}5l=?FI0=du;@K7J8?fQ8syPi7t#( zvbpj6R*~PtHnLw#thk(jJUH-IjY#(JAqd6;O-Gqz!HA1g5fsrGLLHQ zSV6W|y~rshQdi(xgM183#;c&?XiA0FsMm-4SxgO0O4?^c zYeCe>yCp;7fI=#1Yd2%LC=c}h-5s51M~w@vuuO8QEB27Bwa;GhC#}f$xrc=#x|+HV z{;Q=XWq3~lRGUo31NC&%dJ~5R_PkG1c1p=Db7*37g=JO0cFakav6*>0xy{b|ED#O^ zD6FmB+;|V6C-C9s9FXxOl>DS zZig`|OSfoEW8}t_s{lD*nWOJ@PClhLov&9t%=A+nl-ZW|xFAqEf%-K{Ez3_P*W9w^ z+?{kkGRNrVysUt4mlA!3J1R6ts=r@(W86b)G$C7vja$%#;&gLwC#dW!1TD>Mbv{k} zsmky;ywp5PQ=(4hrO{uOFAuxT_~zbfh$LQBs^QT)cY_mBuMh*vL9G>if?n4*no~eH zt3-IwO!!e3ZUnUO%m?(*FEIZ}BDi(v&ph6AnS$?<)@uuQ>$WjioOU&MM1Stdyf$ z9NFis#cC&co>8mjOj6R23q_MllYx}0Jkf$K=<7wCs|4s7KUAv$she+Zp0tlejT*%sQ1aw^mEaH)5o>O?6@?8=8>$BeYAcoeRCyB za#Je(=dj#o?fu6(c!`xd({6{P1tyF>p9+FoF@kJCmuJoay1W~Nt$DP#F{Ay85Oe*; z9?W8p^JQzgzk$4FY3Vss6kVKAui793yf{|~2~v#_#;dL~{^Tm|q!10SSmB_#FDbxm z<`MmDVfn!?V+JaQemB7Xrt%rutNWP4OBbZzILR)F7|@>2h#%eG6*${Vb-mGM z$C#71t5NGY;FM8e$DV<{vWQ-LRwXs`-l>b5<%^N#e#z8Q@X8wab|vU zoNeaXEa8`b-8wD>t=NMBF8nS$unY8sS`N7pj9NjO>~Aghc~5$r&Uj8mb)7bBQ-H|9 zPzKR4mFb_zBXL~Q!1nYH=NDwgBNeyo36;G;B7Hm=!LwDpiDvw4)Qyet`%@Tyd86|_ z#J(S@{uN+zbtdCcGN1D~(Bof*Twc|!Qp4l~Xy4wh4#^ZIh9hE_)qOz0WSC?zxAo|d z=m_GxBh_Jy__BQPb&$@v;_>fcHawg*IL6;_j?mA)4X+^V?qVKWmH_lXMGzfBn+CmF*|H57rIiY=G7pRu%m zOZnWNSrPrk%9;2$d!kHGu297U(hVi`1M@L^~@IYLEp5`iG+;az<=C*skjGBcHxDte{cFjPz3?gzmJ! zAySOpVtp6P2y^8#4aXM^e1QngAjMKH=j%&KR(^|jdVhSQ2%$yekRSIQ%2trlv~H>M$v!Dg2LPQE;!q~TbD3ZeE8WS*Pnl1c?gYzCkAN*AqFXI-`;mqp0 zk)Wgbf?)#V4za8PZ8?cLGt2jX(j@q|-{Jx^$!+Feel;+;37x9>_0%I537G(_RR2UI z2ZkPgJUD3<|G>{tBMe3vDF%)#kd*U=UyET4vBE%zC}>|OjDtR1i0`?#6_||0`!}wo z*QiNNX=GJ<6YKdxnJPLYmMxj=l|0EpDqN@MBLeB|0-#*K%dkW44e0K7*XyPVm^iFy zkDc&HJ`P_^Eh#VZ3%qhfheK?j$iV>wCCRuSCr6lfyOeYH4YnN1NMm4ph@u zZv2^f8w{HyA;FkK&?RLXz2CjrC0QS=gkem!wm0)DR%;XNl2fg3un@zwwv!BDg%lAX z$dBMs%?9Xdi{<2A zT2a%nLqharIE#PInyz?v`u!)xkF(u!8An-xAr>chJ-N1ga<6r#XUE@j>)Swt6H~%y z{UGg^pWzKsDt*x^&l;hmqnt?gbSNPjfw2X^P~*zS9)>NTS#Rx+zrm^62rJ5;UF=|H z{kj+Fgyd&!7Bf^EDT}8o$t#mZy5wlF>Y|V+&*Wr%zr|QsIWgtN9gMLKFMDKn= zZ7<~2pz6?AnLIm1MKYp^?K(vy7|7b2l-GDf5?=^+WqTS1{2p81S*{%$uGxBNaW>O# z4@2ygy|L$LsK4h8(C!v4dR)iC!^i#C95?gJR`cOE5n#m1PpEBbGm{diF8r(_l zoE;{IHJpJ&pMhyaH2%iGJxwYA?_FLSUAk6Incg+5M4mtE`MZ4IUGFDzL>>G0(|!<-|>&p$+f%X=Fs0P`oy2ADrU`Kps{gT43U+sEqW1)1&p8 z+TDhAbND*bs{ER_r>p}fC%oO*O`vBV?yp?)xmUw2QeF&j07ulfw^+0mgIyBHw{OKj zPk*`wp;L<&I~SvcHm%~5mzYE^J_>&&>_dGHBF56*biGx$3H;0NFIB3HWAO}jH6T~8 za^xd`-*;gHcrSZg+TKG1f#q87$JL1n!!^ddwm(vYp$kS{U=|kmt6L&=HRq2=FqjHt z0F2F(uqXdwaW=P0?C0O8r3MOEaTOPbdc_zwtfvsQ&al>;uld# zx!~NLq9r{G^}m5GeL_n@+M-j0O58?x`}0WA^I)W-Yx$AsnU%Ej>ib_g%9#32V`{}4 z)hoq1gVeH)&@0R*Ud94|v_RY#0HiG(<-lyO8l%mLNwpssWkshMfjUvIo$TJC9HBu^tun{9U5}t`Fzu1zZRL{O2|);?SJQIhia{5FP&N7(oS9*tI{+J5(A*^Srkk<_;PG!p9k;gsr4tT}hEcciE?6>&pD-vGK9P zKAYv=EY6lxGEf&&2Gy_TM-$MvO_##hL=0Lc?!!8_P0pIj98M^1>nPri)gnCD@2(1J zt9BzND5geO*i(U(8n}7JbWW*yNZ7rzy$dgd^jy6yGS?&G8IJ1 z5%{YGQYXg8j()0=y>@|VM^07+eW&`%+!9zmoK?IhATjax{hz*zw6c7Hz{nVL+GEDac z2xHw|UBy*mvYp2%X_iWWSv$|nZ>F98Q#t^keCMXH8-eGi&V!9uidX2B3ghsK#HZNx zfCru4L*>VVA3!7!phovX5WX*J*NjR>PFvA?w)Y2U9Aa0z4Mwc6dqcI8)Jdy+gW~#p z*vOh2e6Rq_VD^X8aI0G?f*Uux;c3u6BR4m=Iv4_Dg?45?V{&p0YqS2q^l{F2>_Nl6 z9_?rMad4ncmiGOv@fifDHaUobOZ0grR%q*0xqJqdje3a5JWo*ahTYJh(kUsrO5>SZ z4VF!M2Ue(GE28X$%~;O-?+4wa*Rq2?HCQD)DM8VFEThT7n${J4+#R$xby|>5Y|r-k>>R^e$dz`ug4S3NT|C%Rr#J7yekuKXnHYl;*w)qubX(L-g)x@oX15Tz=-=s8ZM zRm}+Yc}<|KF96^HC*oviXXf=8hjedJqk=5@1pcbu>UiKz9FlRMkF?8{$POd3pf2>#XPXucE{y z4B3cf+tOVBC+~6zNBJvJ&6OahqGXt)8x~= z!aI*FdZG`~Q86YE8U`Gp!do&)eNBJ}@IX}b{VLl~4EWbg_yf^@c|b3NT1OvhgEq#r ze9f_1#6k>^ceFs<0NH;3j>`U@mtv2v@}K_2Q3>p<(0G$f_5d6V5O6s8|8%fd778K3 z1PpjxbGcfBI4jg34yp=ce_=~@W@*mKmsnK8X+UNA-1QF$2-u-`x1r~RX%)()iOqDR zKJqvf3IX(|o1g{*EE3Ip`RXOAz&?-2Y>FT3D>1Cp&!1w@waj)y9fEH3{KbvEjJ%gQ zLE)3-12mnfdzni>6PNNzK@Qd5WDy1cpD^f}BTJb>oo5kGO78Ss*LD$I#=wsL}*4+1>gpdP&aoeoTvw@sYVlFbz%nD_20Lxa$ zDhU;M*&9j(Xc`dM=do1@^8muzG$687)2Zd9JtN+ziNCnzFx;LJfW(#q2yMERrYCrJGAPnU|;W8$va|38n)wPPNwkH1&^uBKmAjuH42B@jfCmts7&hg*J|Mc*m s5&R#VAyk}(xT8%C3X!8@5d zMn=f#;n5jx%am7fLU%t*zkD5d=lBR-?nzbar8F4}8K&2g96tV$e$`TzQk*|Lp8C<{ z1@`zoh1K8Y9KST>@#Z{J-H6%IjgK<=Jc@76^n8A(ZKCnnQI~Dz?EEoJ>Ez#c_QQBy z#9fSF-x&*b>e|`tdWTrK<(0pwG)u-QSlc!QOxJvjWriS&Z2T%0*!hGoc(Xfua3oHl z|9t(A9R6bj|4V1cDkVrk&1ts-&9XG6)8SS%TTY#&*_I8oLA1gunH$xm4ndn_xAFjfuMc~sRX!?;MMapA(m zIj38nUU9Khlkd81XU3hmBRP?0*sRa*n^d%T4=&)_oH#)-hHl2@y#B1_)n*{h!3;q>BT}GA&#byZNY=6^^Wy<&0o6 z34c)S`4wUjbnn?vAc#S#hh-ODbFqwCc3|x-8~d&eye4fZY|VJbee@D5cFT`y&zHFm ztsSO=*52r)Q^&tYGPXoA=G;0Bk*`CLGRH-n>wb&(XpqRN%esw{^Yf=)O0(U?xO}vxL-%{wajm@@U?O?%?VWOwnv8QEKAarFr2{ zEOYn8>(O;_T)2zLsdg1uEg)heKeTrJ_Ge|NCW;$s7Eu{^&D4cRrtY8kJ4d=n$K4%Qq7(#ja7RE+FgnlVTLY&WAc7C!NS4G+#BPZT%h4|FsPL7 z=h!pwfZXYwpY1tM=T#1wwy$jNC!nf;00+;SrI}l`K2LlS?Qy@tps)* zOylT7vB$gRz*0QVczko(+KN;gM1NGhsH^l{QWDGS6^n}cECChK(Lptba`*9?4HkEn z>TR9wLKgHe=fzrj4W!gRHCXd!pLg@rwkv0{Usnkdxs=EZx z;5Y$kNO^=~Fl%9jN?~r4bdiegJ3%43R*`|O^eFsgdT0zt)^#_>vwP1!6X^SQVpG=b zz`<2>I#@V2q^9#cRH>_JYRBz#&#TsnvZFSUJCd<4ZbXxmE{QMXn<;t}W*ZkfgXzw$ zv~MSwVM0)|SC)n~nA5x6wrh5r{#SaCNLdBfM6t?xS+5b_%}0(w3KA9G?%!j|F&Bc| z^^Nxh9JL@bVW`;>%o{0Ntr$0LtPDT{3j=~GXxWc8udNRIzWnqRtWvk_55^$B zOm07)nP%aX&WDuHQ_aQ_bj@PGBzvdrL$R#|mco<~-70q_N`mWp=)e?3%wgxEQHCi} zmFq|H(?K(JEwH)g+>nLvrv&PZiKTaPQG0$A0W%d5a0SzpdSB3a_$-tPv>^VlwGC!E zPheH5lS}F$W`qy&nOf;PnCLt71NlypUEGuFiXMA^SsjWaM)JY--Wp)pdSd{*4qHBOoEe$z!e`a(m) z#9d-8H9m1`Y)60Y<`-J{3*ZnAFw4tFYMz3Pyz9{0o!B!!PM@N@VK`I!>~BWZx#J54 zc_LZ^tzk_Y3m>UdFY}1y$)7Q+1BnL?y0*m!8H4>?1LDLCP=T+0{c+(?M^WEOk5wL? zAYo2~+o%>}5xz#1&vOktD@{)SNccK;lL7KMiQU;tG8Tua(^HvrHg{j%Sw|?#La1y3 zZOY3+T8zgmBC#8Fp5=7mPWoQymlcoDL40TXKkfTfA>H~tf2pgUXFJMH*uvS0ckRmY za)3C5u#c{r#WCp$LYd2z9$TB)o9I#6rv4!lC&H8R^a^9y94Pc+a7=QZWb2!QKr-ejFyF0?GH>L z4wPWB%lo)m5jD_ow`8t%HNCrE)>G>AgJ3~L5GCBjjWV`E&dDYIblv#Tmb4*1XX6W1 z7s_!GXW`do_I|7DNE@7F{D3$Hb-xGE(Jv$5XP=Wk2zWiE{1&*Y&DyqszD9(UhNTVs z*m&0oDMNNt)h$TPN;@_-FbETJ|Dj1=@X?@cRP2d%k{0X z!*($Pqq4|Ai^M+q|$w2>d2Pe$Az~it7(tX;0&tKz(xraljDh}6v z*~iohQQU`3Z1tBrT0d75*!&XT=dFjg|Khj|{`2?h-KvNWlE5B0d#5LKDF&+ww`Z|O z^4!$J$3=3kd%Bd?|JhkA!oN>%w5*xv9~*JgK%W5nQc=y7=cl~snT0$AA;Gl=IW7hT z)LHN_QgQc6I(3<}?K$vuEBCLIwLB^9pL}fzQZyKz3RF1hpgink9l7)52Tt!`N6%mL zuqK=w3?pEs3zlQI^HqoLP~6MQwUXDb=qXd*^*H@AD^tY#L@PVlJ!Cl(Jk5mK;vy->8Ljdy-|PbO&BdeH>v&vgmKw}Qy7{NMx@}t;z zk36ez0sVzePg_~inIo`cK4%!Cnze0|of_6WLAo%TSQx^a_64UOR#`7eOkg`zG*7k) zs#4I7dL&JX0`TN#nvOZyb1;Lh9oc&Uu(zgi41I;csyb_)X%+i2y`ZM;pS zKW?$H>n1*yR46Z07dmY%CV7dk+1PTdYHC(zf8BsjDRc{4n+8AvH3># z8Voi__(SiMTqD9hvOThFMxUT(u5-#-Qu6r+=@ZoxYg~)I72cl=Kzx%SJ*A8$$6)d; zdjCkMIcH@Bly#RW8Fi~NLu=!MLfN!>?jxtR-t_Ue*cTtgRt)F$PF2kuqXeD8q5WqN zVne)&(oamC&XIW0DR#e_SZ@xtH3&>A1uwnWF(wOb+cIh~jfn|BR#dOp9bm#a)b)unp?3+*B2ES6w(0IvLG=()doVmTUuY?BwrA}&EVb$jR|+s3KT_mG$}SBW~)5{c4`WP@Dx5Y84zdA%C9cB{p8=U#euy z0xUdVjdS1nuC5s2On-u7@36EZs}D8qq|U^Cdg3=V`)c4b4npSr_0#UwXfN6uk@s|y z@-QgG-iw4R(W5Bx|Tu>GSLAan$*7}Zw$^f-?TritruNO zRh6J`_GEx|&&x@>e;n748GJuPVJhDA^d;9iT1N9uP)zSXV9JE2Pplz>WfM$=f-Ln1 zC^TCTy50ARKS*;xlf1^+G%wd_FeS&GvGQ zTobqTrm8xk=!JA-Q*y5xOFd$FSHznU6`?0VciTvE0%O^L+u9dPbG`L;yk=%momVW&S3y0ebFP*_r3J2Fpu5AKf9&K+Ah;wP{y&6WV4;@ zR9TKJq8Ee}t4sSfoiGXc(B*5&OO{r-O;?PuCz{8BE)=T8^R2`p><*zON>W1eXQO6u z=>^wF+c@?^uEj`M&(lfN#Iba`%UjXOq;zQI1q9)eKLwSv?8A^B7wlCq^2W8SDV>>^R}uO75@eY1h*=beCtQ=4|6q^kZG0?1Z0GqMJMfgOr^f)Je}}BjI9H z_`WSIPjzP&nQb|f(zLdZ&C1tWbrKjX;2We_>4vxg)X&ZU1&D!raYgsz#&JOmjs5xS z3^OtgXTFtBAWM9PF5tea)=5d}+~ul;dup1X0PI9Ffl&|eH@4|`af`84e%@^Wv< ziD(`+=zNpH5xU>9K(?n`Yiyy@-t)OG<5VDmvTOdwnJOT*7ojI2k^lT9{F$){VR3+c-cwSCLa)5(h{rL`Z+FFRP}l1^0$vjc`Neg1VC58Wt@X46Iv z+~0H54i})PsBHA)MOV;vkb9zvds9PE72^-Qh-e=E!S^@6-o{r;uP+-iGfM%8J{aZ& z-I2R@r=rl2$UVb*g;G&w5J&v!1jo<|kR60^dkbi4R@s4~ve)g~qADb{>NPc^D-rdX zA;HZW#~_rRR~=^sZY_hx_ zZt6R>Q&C*hgJp(1Z+h(AeW-H}y;qLC!UOASAnFZ`if6zvzg}FTylvp4(5Isgv6W?qE$ZTgc@!_C@0-}^h-Ne znsYU21H8x5t%d`KkZli{ovno%MmQcX3|}#Ibi5Z-=pO`(Ve4jg>!__>_7H^!>VlyX z0UCD;<)>e2nWN=JH@c276E+Evk0KW1anyXFvf>i#H^&>zx>9lGljyytOxWqtS)3NoUAk0P)yQNN6G?e~zaM@vzU1(As zBF-TtjcJ8f_+U+>TKK&owy?YyzP?9amLwtsfVE*g*&nmeK^XuV58=xpQ(g3X(wRo7=heM7o3087A3rZh6`P!c(- z$IBLw#eS>;-J)npgN~<9VdXPaP-Vet=z63^24U)EcF@uZ^kot&#?TxBc7gKM_QP?j zY_0Zp_EF^)o8Q{@h|<;!tX&rUc8<`sVb>V#+bC5_@Sy(9c4THe2bj>#b8jCfZ@hYK z%}JVnZ&)|_UfldleQ&1JdVA+`zP&_;K>0ZElK+s$85A8pD_$J^~QCIussCf}L-xHpS-%eCgC zV2D53RI^-jimD2YzWcC9MAgvjDw$QE^lOsTz$b?QM8GWyWEB1=jMbncE^F@ zTxStoH?lzb(<`D7B2v!^p7bKBZ6mzSY%Z>++w2Z?VR@Ym{)v*Rv%^vH*;kBPkT%g& zb1z|r3PsxOb1S`8f|@;<+)f!_eD0kx3}+&{ALdv+|%hjlo8lk z?o^x*;TgjN1pq8(kZd_C6L>)fq?q+f(#x}m6soOD@may+5pGV~l#G+)O66^0-_uVd z+k=eqU-8rRiFDuiKPmg*qFwdY4<`I7*lGCcA?^)@2`6J+PxM zQ^2mgscTn79`E{ItwAS9R)bKpVn6Br@x!P`2boE{h;azd)=Q)-S`mylj=^79eTAr4T;Bn zG^bdBA+U1)Co@DKwL9y^Q%RE5MH?TQ+z$ zP@Q~&pdE>is;KAOay32J@yK)=B6Y>nC9liYl2oN@cj#X{3d*3uzR=r^G`C++Ba;Vh z)0;y3I-C?r{>swiPd3{3r5@PWGBVmZ3akS#9Kj@xe&JU=yr2NFnDjR`RM+ZwgkI$d z`BM7KhMJZwxz$|RDJ%#=?Hy5hxTbi&MY2dfq1j*T3h5;lKyp2W1i9l-uRKrCN-Xtu zJ-fi*N>2|PK^WWFe7qSi-*6b(l$Nf$EXVmOpPDdQwwc&rLetT~(xYs_e?f-`{w6xNB!!ymS4acWfO{ z$H^l)>D1i-yWBm`M3Db`a3Q(NibyRD$NY~uJ#a4Q3kX2F3zlN`=b$sEZ-0W5E_IqA zLx16<#?nqb{$Qx`c&XKr|6=&81;#LtI4;%7?oJ zY1+T~#pW=q?DOK7lF%aeY)k8-^u4{zzRnMC=Bot1yPX&Ph%O}fL>~>vNDQR;slCmh z=>c>4$`Yh}tO~_vGIpmi_336_bY&+AP6oIWqkD;7IZ0cLM7m|lyJPc9N?-pH`swP; zY!We#e;{1ojL#N=$Bpb;XkM7oIqU1-WmLY`;CSSDbL@uaEEqMJ2j`oMM68JPe{x$) zXn0$dOx5H7{-I?23wrzbwx-`ox4ftnW)bUVQdnnLfBxqPMd0r~)D|Mvsy0sHi&)c%hYUbuc`NGCCOP==+n zx8&?j>>?lBPZznrC0$a@d#+dqF+^VV$Mlh;Ufe0r?F=QwduN_pwSx zy0FP92Wcgiu&J^DrXUBz!R)y_>Q=N2mND;NM}96elI_ddm3)7D z4h`RbWW}{#{18CIWXj;vEl|JXIA94_ts{93za;ht`*GL#-l&A2qag>q04{jHRfQlE z?DkT*J9Yxw3kVW$4+Mln8YQ+F%)P8#KUUk~DI1%6$Ojp+j9)vt)QB6d_X)EQ%Ce=5 z69%M!#x!;Js=>vx^(ggiN9c5O5Gc=`E>A8>x!-27Z_p?THd=X2`pLl1Y<-UG0%4Ai zMZIdeL%em=;{{)*BbmbVG=Xe;n$hfn!w$Dn$jo>|4ZjZioal2G;bs3is*j~Wz~AJU zloi{2INJD8D=1;LUnOal0x4X`u?|k?&^v7vj&MbqVf1iC!B&}_Y0f6>DtiyxPVN<- z2O3}N(6V2a{}!Wtl6KaP8Df4n_+HI;u@opCtteB31>pMawo>Ph&KPX`;GTIrzkoES zqKM0-c!xy+7(njL$rIk)&SwXoKKV*ZtET+*+Hrm-Gmp$p9?!RUkH z2cY-l46O7RE(1tb7h@Y2S9{nXmM{sc}g(WeCa zm{U#{DrY>HI%B2uw=n1RBAwuc2NBQsgEq9-qN995o)FT3CP$wPj%6St+K~(bpaeJI z6C~3(o4Jntgxp_KSJ`RoDmnCTOn5V~xjW5QAB!voSIy6GQq20SE+{e2z+us#2r2Hc z;{teAuY$k1UBbWLgzm3}lQsp?u5Pos%)h>mF`z%A=yUsqu<)pg;=;UlxP;O+Uv{~6 zTz^gNGe!n(uyCRPx2a#p(ku72!6YC}iv!A_*N6>;e_-g{b_UbGKy=TT!p3*(CJ8xg z4qctqWWL9&3gmo)uUu^2qIN1R8f&gjCklLCvepFlRN+Ooy?~um3Fe>Vgh8W5fUsd>*SzPMp410u zgj5gFiCfTR7r-GJDA6=zGzQ?KoPadSpC``sW@{=V{(?b4i{JkqeXHe%UGBwC`T;t? z0PqVw^F6{Qlx%l!DXju=OK%xD+~?EfSJ@Gd}%~D2b zrC^`|`fyDIxM-WTuk@w0En>E+dE2qqib;50d_=PZDj4u_%aoNN6@>}^oLb%Yl1S%Z zaJ0spw@FHQOT_~Z>Ugl(gvu*2QzZ} zvH|_;`l7M+kyB3cdbKRYKzh;g`01rf+#%UrT(13F%@M1?USfZiQ|>#>TiK5b@Sy-v z+3gebE*K3zJ4rIp+vCdfrF4#_&{Dr}oV634YH8RG-C1mvdSR(2y#yFOAL<+jARMDC z8wh)AVwM!;I-E`uDgs(>Js1sGL1Rld5NQGv%%lY3mQ#_QVC53l1jk| zry~mSwZ327FPQYZF`)dJ$5}l?smP(PesG_wFi6pi`qv0W;QI;czI9Ybz4hNoQ{T#F z1TPO;_qvTPFV-_^fyR@KDFs0MG!<`RWDiMiB-HIZj#s?UeTTgc(5& zUspgO`Q#)Mx5mkw6*0CSHed1q4mUvkFkNr4dgc!GRqy^pcS318U1pwTPxD?MAU_0> z{u!qNnD*oN zQkHS;WM%CJmr`Fx>AR-Wh<56n(o+^XNq}Yj-X*>lx0I#_%$iul(`No#ysrJHQj^V! zWccRZQnibx5qQ~$bg4aCGm-K!I;2+wDOi9cC>1Jgr>n*4n;S>&hMMR4`u!x$1A>ueVoxptm5@6o5)UTR7P4gF8}(B;}r6Dz{I(atyR>xgs52KS$)*06EZs z*h5MENeO*3>6vk-@BGd(B=~7T>Y5O=k_mbeK+6Sa5jf~&aHJo-2{Z;wz=!JxZl z=MD%J_s$m*cauQ$C1AqqFZ|mE)5w0%^HR!GJcc-(IEBm4bU%K-_~KrMAPND0K#? zITHkKfWtuObH@hgTVehrpQ;H50<8QJH8EP5D7{a|&H+K}{Zo|o*eJ0I+I`R^= zDIx?RW=IW^N#{>6<_3RSM*p{u#^u&8Z+9+cAkTtzRbX@l=MGu)9fG}rAO_Gu2Dl02 z-YY!lMgvzip&5mP_OW+P^v1fwtV+O_JpG6W@G(-zUO@+gx|tSwk`{V~ds)9^半透明 引用 查看我们引用了哪些库 + 背屏自定义界限管理 + 按应用管理背屏显示界限、DPI 与旋转行为 + 选择应用 + 选择需要使用背屏自定义界限的应用 + 背屏自定义界限管理 + 为每个应用单独调整背屏显示界限。仅作用于背屏显示。 + 暂无已配置应用 + 编辑应用界限 + 当前应用:%1$s + 调整此应用在背屏上的显示范围。对齐位置和旋转角度使用固定选项,避免写入无效系统参数。 + 为此应用启用 + 宽高比 + 模式 + 可选自动比例、自定义比例或精确边距。 + 自动比例 + 自定义比例 + 精确边距 + 自动使用背屏自身的分辨率比例。 + 左边距 + 上边距 + 右边距 + 下边距 + 对齐位置 + 选择应用界限在背屏容器中的停靠方向。 + 缩放 + DPI(留空跟随系统) + 旋转角度 + 选择固定的背屏旋转角度,或保持跟随系统。 + 请填写有效的界限配置 + 界限配置已保存 + 比例 %1$s · 缩放 %2$s · 位置 %3$d + DPI %1$s · 旋转 %2$s · %3$s + 跟随系统 + %1$d° + 居中 + 靠上 + 靠下 + 靠左 + 靠右 + 自定义 %1$d + 跟随系统 + + 90° + 180° + 270° + 比例 %1$s + 模式 %1$s + 边距 %1$s + 缩放 %1$s + 位置 %1$s + DPI %1$s + 旋转 %1$s + 已启用 + 已停用 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7360034..c929560 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -500,4 +500,58 @@ Widget: %2$s · Priority: %3$d Semi-transparent Quote See which libraries we have referenced + Rear Screen Custom Bounds + Manage each app\'s rear-screen bounds, DPI and rotation behavior + Select Apps + Choose apps that should use custom rear-screen bounds + Rear Screen Bounds Management + Configure rear-screen display bounds per app. It only applies to the rear display. + No app configured yet + Edit App Bounds + Current app: %1$s + Adjust how this app is placed on the rear screen. Position and rotation use fixed options to avoid invalid system gravity values. + Enable for this app + Aspect ratio + Mode + Choose automatic ratio, custom ratio, or exact insets. + Auto ratio + Custom ratio + Exact insets + Uses the rear screen resolution ratio automatically. + Left inset + Top inset + Right inset + Bottom inset + Position + Choose where the app bounds should anchor within the rear-screen container. + Scale + DPI (empty follows system) + Rotation + Choose a fixed rear-screen rotation, or keep following the system. + Please enter valid bounds settings + Bounds settings saved + Ratio %1$s · Scale %2$s · Gravity %3$d + DPI %1$s · Rotation %2$s · %3$s + Follow system + %1$d° + Center + Top + Bottom + Left + Right + Custom %1$d + Follow system + + 90° + 180° + 270° + Ratio %1$s + Mode %1$s + Insets %1$s + Scale %1$s + Position %1$s + DPI %1$s + Rotation %1$s + Enabled + Disabled From 9364aad109d6defbb96d252dcde292883754d8d9 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Sat, 9 May 2026 11:38:35 +0800 Subject: [PATCH 18/27] feat: use widget info for specific release versions --- .../rearstore/RearStoreRepository.kt | 112 +++++++++++++++--- .../uwu/reareye/ui/screen/RearStoreScreen.kt | 38 +++++- 2 files changed, 128 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt b/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt index 34123f2..0da2a4f 100644 --- a/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt +++ b/app/src/main/java/hk/uwu/reareye/repository/rearstore/RearStoreRepository.kt @@ -902,6 +902,8 @@ data class RearStoreRelease( val publishedAt: String = "", @SerializedName("url") val url: String = "", + @SerializedName(value = "widgetInfo", alternate = ["widget_info"]) + val widgetInfo: RearStoreWidgetInfo? = null, @SerializedName("assets") val assets: List = emptyList(), ) @@ -1128,18 +1130,19 @@ object RearStoreRepository { suspend fun loadWidgetDetail( prefsManager: PrefsManager, - widgetId: String + widgetId: String, + widgetInfoVersion: String? = null, ): RearStoreWidgetDetail? { val normalizedWidgetId = widgetId.normalizedOrNull() ?: return null + val normalizedWidgetInfoVersion = widgetInfoVersion.normalizedOrNull() val baseUrl = resolveRearStoreApiBaseUrl(prefsManager) - val detailCacheKey = cacheKey(baseUrl, normalizedWidgetId) + val detailCacheKey = cacheKey(baseUrl, normalizedWidgetId, normalizedWidgetInfoVersion) detailCache[detailCacheKey]?.let { return it } return withContext(Dispatchers.IO) { coroutineScope { val descriptionDeferred = async { loadDescriptionCached(baseUrl, normalizedWidgetId) } val authorDeferred = async { loadAuthorCached(baseUrl, normalizedWidgetId) } - val infoDeferred = async { loadWidgetInfoCached(baseUrl, normalizedWidgetId) } val releasesDeferred = async { loadReleasesCached(baseUrl, normalizedWidgetId) } val description = descriptionDeferred.await() @@ -1150,12 +1153,17 @@ object RearStoreRepository { } ?: return@coroutineScope null val repository = description.repository - val widgetInfo = infoDeferred.await() + val releases = releasesDeferred.await().orEmpty() + val widgetInfo = loadWidgetInfoForVersionCached( + baseUrl = baseUrl, + widgetId = normalizedWidgetId, + version = normalizedWidgetInfoVersion, + releases = releases, + ) val metadata = description.metadata - val widgetType = loadWidgetTypeFromInfoCached(baseUrl, normalizedWidgetId) val resolvedAuthor = resolveAuthor(authorDeferred.await(), repository) val detail = RearStoreWidgetDetail( - type = widgetType ?: metadata?.type.normalizedOrNull(), + type = widgetInfo?.type.normalizedOrNull() ?: metadata?.type.normalizedOrNull(), widgetId = normalizedWidgetId, name = description.name.normalizedOrNull() ?: widgetInfo?.name.normalizedOrNull() @@ -1166,7 +1174,7 @@ object RearStoreRepository { widgetInfo = widgetInfo, metadata = metadata, readme = readmeCache[detailCacheKey], - releases = releasesDeferred.await().orEmpty(), + releases = releases, ) detailCache[detailCacheKey] = detail detail @@ -1174,6 +1182,22 @@ object RearStoreRepository { } } + suspend fun loadWidgetDetailForRelease( + prefsManager: PrefsManager, + detail: RearStoreWidgetDetail, + releaseTag: String?, + ): RearStoreWidgetDetail = withContext(Dispatchers.IO) { + val normalizedWidgetId = detail.widgetId.normalizedOrNull() ?: return@withContext detail + val baseUrl = resolveRearStoreApiBaseUrl(prefsManager) + val widgetInfo = loadWidgetInfoForVersionCached( + baseUrl = baseUrl, + widgetId = normalizedWidgetId, + version = releaseTag, + releases = detail.releases, + ) + detail.copy(widgetInfo = widgetInfo ?: detail.widgetInfo) + } + suspend fun loadWidgetReadme(prefsManager: PrefsManager, widgetId: String): RearStoreReadme? { val normalizedWidgetId = widgetId.normalizedOrNull() ?: return null val baseUrl = resolveRearStoreApiBaseUrl(prefsManager) @@ -1477,6 +1501,11 @@ object RearStoreRepository { preferredAssetName = assetName, ) ?: error("No downloadable release asset found") + val installDetail = loadWidgetDetailForRelease( + prefsManager = prefsManager, + detail = detail, + releaseTag = selectedAsset.release.tagName, + ) val assetBytes = downloadAssetBytes( baseUrl = baseUrl, widgetId = detail.widgetId, @@ -1484,10 +1513,11 @@ object RearStoreRepository { onProgress = onProgress, ) ?: error("Failed to download widget asset") - val isWallpaper = detail.widgetInfo.resolvedType() == RearStoreWidgetInfoType.WALLPAPER + val isWallpaper = + installDetail.widgetInfo.resolvedType() == RearStoreWidgetInfoType.WALLPAPER val embeddedMetadataBytes = if (isWallpaper) extractRootMetadataMrm(assetBytes) else null val defaultWallpaperOptions = if (isWallpaper) { - detail.toWallpaperMetadataOptions(detail.defaultWallpaperName()) + installDetail.toWallpaperMetadataOptions(installDetail.defaultWallpaperName()) } else { null } @@ -1519,14 +1549,27 @@ object RearStoreRepository { wallpaperMetadataOptions: RearWallpaperMetadataOptions? = null, onProgress: (RearStoreInstallProgress) -> Unit = {}, ): RearStoreQuickInstallResult = withContext(Dispatchers.IO) { - val widgetInfoType = detail.widgetInfo.resolvedType() + val selectedReleaseTag = preparedAsset?.release?.tagName.normalizedOrNull() + ?: releaseTag.normalizedOrNull() + ?: selectAsset( + releases = detail.releases, + preferredReleaseTag = releaseTag, + preferredAssetName = assetName, + )?.release?.tagName.normalizedOrNull() + val installDetail = loadWidgetDetailForRelease( + prefsManager = prefsManager, + detail = detail, + releaseTag = selectedReleaseTag, + ) + val widgetInfoType = installDetail.widgetInfo.resolvedType() if (!widgetInfoType.supportedInCurrentVersion) { error("Install mode '${widgetInfoType.rawValue}' is not supported by current version") } - if (!detail.widgetInfo.supportsModuleVersion()) { + if (!installDetail.widgetInfo.supportsModuleVersion()) { error("Current module version does not support installing this component") } - val requirementsResult = detail.widgetInfo.evaluateRequirements(context, prefsManager) + val requirementsResult = + installDetail.widgetInfo.evaluateRequirements(context, prefsManager) if (!requirementsResult.satisfied) { when { !requirementsResult.appListPermissionGranted && requirementsResult.missingPackages.isNotEmpty() -> { @@ -1556,13 +1599,13 @@ object RearStoreRepository { else -> error("Widget requirements are not satisfied") } } - val conflict = resolveInstallConflict(prefsManager, detail) + val conflict = resolveInstallConflict(prefsManager, installDetail) if (conflict != null && !forceOverwrite) { error(buildInstallConflictMessage(conflict)) } val prepared = preparedAsset ?: prepareInstallAsset( prefsManager = prefsManager, - detail = detail, + detail = installDetail, releaseTag = releaseTag, assetName = assetName, onProgress = onProgress, @@ -1576,7 +1619,7 @@ object RearStoreRepository { RearStoreWidgetInfoType.WIDGET -> installWidgetAsset( context = context, prefsManager = prefsManager, - detail = detail, + detail = installDetail, selectedAsset = RearStoreSelectedAsset(prepared.release, prepared.asset), assetBytes = prepared.assetBytes, conflict = conflict, @@ -1585,7 +1628,7 @@ object RearStoreRepository { RearStoreWidgetInfoType.WALLPAPER -> installWallpaperAsset( context = context, prefsManager = prefsManager, - detail = detail, + detail = installDetail, selectedAsset = RearStoreSelectedAsset(prepared.release, prepared.asset), assetBytes = prepared.assetBytes, metadataBytes = prepared.embeddedMetadataBytes, @@ -2187,6 +2230,38 @@ object RearStoreRepository { return loadedWidgetInfo ?: widgetInfoCache[cacheKey] } + private fun loadWidgetInfoForVersionCached( + baseUrl: String, + widgetId: String, + version: String?, + releases: List = emptyList(), + ): RearStoreWidgetInfo? { + val normalizedVersion = version.normalizedOrNull() + ?: return loadWidgetInfoCached(baseUrl, widgetId) + val cacheKey = cacheKey(baseUrl, widgetId, normalizedVersion) + widgetInfoCache[cacheKey]?.let { return it } + + val releaseWidgetInfo = releases.firstOrNull { release -> + release.tagName.normalizedOrNull() == normalizedVersion || + release.name.normalizedOrNull() == normalizedVersion + }?.widgetInfo + if (releaseWidgetInfo != null) { + widgetInfoCache[cacheKey] = releaseWidgetInfo + return releaseWidgetInfo + } + + val loadedWidgetInfo = fetchJson( + baseUrl, + "/widget/${Uri.encode(widgetId)}/releases/${Uri.encode(normalizedVersion)}/widget-info" + )?.widgetInfo + if (loadedWidgetInfo != null) { + widgetInfoCache[cacheKey] = loadedWidgetInfo + return loadedWidgetInfo + } + + return loadWidgetInfoCached(baseUrl, widgetId) + } + private fun loadWidgetTypeFromInfoCached(baseUrl: String, widgetId: String): String? { val cacheKey = cacheKey(baseUrl, widgetId) widgetTypeCache[cacheKey]?.let { return it } @@ -2252,6 +2327,11 @@ object RearStoreRepository { return "$baseUrl\u0000$widgetId" } + private fun cacheKey(baseUrl: String, widgetId: String, version: String?): String { + val normalizedVersion = version.normalizedOrNull() ?: return cacheKey(baseUrl, widgetId) + return "$baseUrl\u0000$widgetId\u0000$normalizedVersion" + } + private fun selectAsset( releases: List, preferredReleaseTag: String? = null, diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt index a679de4..1cbfebf 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt @@ -1125,7 +1125,13 @@ private fun RearStoreDetailContent( loading = true loadFailed = false detail = withContext(Dispatchers.IO) { - runCatching { RearStoreRepository.loadWidgetDetail(prefsManager, widgetId) }.getOrNull() + runCatching { + RearStoreRepository.loadWidgetDetail( + prefsManager = prefsManager, + widgetId = widgetId, + widgetInfoVersion = installedWidget?.releaseTag, + ) + }.getOrNull() } readmeLoading = false readmeLoaded = detail?.readme != null @@ -1133,7 +1139,7 @@ private fun RearStoreDetailContent( loading = false } - LaunchedEffect(widgetId) { + LaunchedEffect(widgetId, installedWidget?.releaseTag) { reloadDetail() } @@ -1157,14 +1163,28 @@ private fun RearStoreDetailContent( return widgetId + ":" + release.tagName + ":" + asset.name } + suspend fun loadInstallDetail(release: RearStoreRelease): RearStoreWidgetDetail? { + val widgetDetail = detail ?: return null + return withContext(Dispatchers.IO) { + runCatching { + RearStoreRepository.loadWidgetDetailForRelease( + prefsManager = prefsManager, + detail = widgetDetail, + releaseTag = release.tagName, + ) + }.getOrDefault(widgetDetail) + } + } + suspend fun performInstall( release: RearStoreRelease, asset: RearStoreReleaseAsset, forceOverwrite: Boolean = false, preparedAsset: RearStorePreparedInstallAsset? = null, wallpaperMetadataOptions: RearWallpaperMetadataOptions? = null, + installDetail: RearStoreWidgetDetail? = null, ) { - val widgetDetail = detail ?: return + val widgetDetail = installDetail ?: loadInstallDetail(release) ?: return val assetKey = installAssetKey(release, asset) activeInstall = RearStoreActiveInstall(assetKey = assetKey) val result = runCatching { @@ -1238,7 +1258,7 @@ private fun RearStoreDetailContent( } suspend fun prepareWallpaperInstall(release: RearStoreRelease, asset: RearStoreReleaseAsset) { - val widgetDetail = detail ?: return + val widgetDetail = loadInstallDetail(release) ?: return val assetKey = installAssetKey(release, asset) activeInstall = RearStoreActiveInstall(assetKey = assetKey) val preparedResult = runCatching { @@ -1286,6 +1306,7 @@ private fun RearStoreDetailContent( release = release, asset = asset, preparedAsset = preparedAsset, + installDetail = widgetDetail, ) } @@ -1294,7 +1315,7 @@ private fun RearStoreDetailContent( asset: RearStoreReleaseAsset, forceOverwrite: Boolean = false, ) { - val widgetDetail = detail ?: return + val widgetDetail = loadInstallDetail(release) ?: return val blockedMessage = resolveInstallBlockMessage(context, prefsManager, widgetDetail) if (blockedMessage != null) { blockedInstallMessage = blockedMessage @@ -1314,7 +1335,12 @@ private fun RearStoreDetailContent( if (widgetDetail.widgetInfo.resolvedType() == RearStoreWidgetInfoType.WALLPAPER) { prepareWallpaperInstall(release, asset) } else { - performInstall(release, asset, forceOverwrite = forceOverwrite) + performInstall( + release, + asset, + forceOverwrite = forceOverwrite, + installDetail = widgetDetail + ) } } From 176420132645af9ea70aca79aed62b415a26f4cd Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Sat, 9 May 2026 12:02:47 +0800 Subject: [PATCH 19/27] chore: improve about screen gradient effect --- .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index dd85539..94b90b0 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -49,7 +49,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -141,6 +140,7 @@ private val AboutDeviceInfoCardBottomPadding = 12.dp private val AboutDeviceInfoRowVerticalPadding = 8.dp private val AboutDeviceInfoHeaderBottomSpacing = 8.dp private val AboutCardSpacing = 8.dp +private val AboutGradientFadeDistance = 389.dp private val contributorAvatarHttpClient = OkHttpClient() @@ -264,7 +264,6 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { val versionText = rememberVersionText() val contributorState by ContributorRepository.state.collectAsState() val lazyListState = rememberLazyListState() - var logoHeightPx by remember { mutableIntStateOf(0) } var route by remember { mutableStateOf(AboutRoute.Root) } var animateRootContent by remember { mutableStateOf(true) } @@ -373,9 +372,7 @@ fun AboutScreen(bottomInnerPadding: Dp = 0.dp) { versionText = versionText, entries = entries, lazyListState = lazyListState, - logoHeightPx = logoHeightPx, animateEnter = animateRootContent, - onLogoHeightChanged = { logoHeightPx = it }, onOpenContributors = { animateRootContent = false route = AboutRoute.Contributors @@ -420,25 +417,24 @@ private fun AboutRootPage( versionText: String, entries: List, lazyListState: LazyListState, - logoHeightPx: Int, animateEnter: Boolean, - onLogoHeightChanged: (Int) -> Unit, onOpenContributors: () -> Unit, onOpenLibraries: () -> Unit, ) { val scrollBehavior = MiuixScrollBehavior() val hazeState = rememberAcrylicHazeState() - val scrollProgress by remember { + val gradientFadeDistancePx = with(LocalDensity.current) { + AboutGradientFadeDistance.toPx().coerceAtLeast(1f) + } + val scrollProgress by remember(lazyListState, gradientFadeDistancePx) { derivedStateOf { val index = lazyListState.firstVisibleItemIndex val offset = lazyListState.firstVisibleItemScrollOffset if (index > 0) { 1f - } else if (logoHeightPx <= 0) { - 0f } else { - (offset.toFloat() / logoHeightPx).coerceIn(0f, 1f) + (offset.toFloat() / gradientFadeDistancePx).coerceIn(0f, 1f) } } } @@ -464,7 +460,6 @@ private fun AboutRootPage( entries = entries, onOpenContributors = onOpenContributors, scrollProgress = scrollProgress, - onLogoHeightChanged = onLogoHeightChanged, lazyListState = lazyListState, onOpenLibraries = onOpenLibraries, animateEnter = animateEnter, @@ -523,7 +518,6 @@ private fun AboutRootContent( entries: List, onOpenContributors: () -> Unit, scrollProgress: Float, - onLogoHeightChanged: (Int) -> Unit, lazyListState: LazyListState, onOpenLibraries: () -> Unit, animateEnter: Boolean, @@ -735,9 +729,6 @@ private fun AboutRootContent( .height( logoHeightDp + 52.dp + logoPadding.calculateTopPadding() - scrollPadding.calculateTopPadding() + 126.dp, ) - .onSizeChanged { size -> - onLogoHeightChanged(size.height) - } .onGloballyPositioned { coordinates -> val y = coordinates.positionInWindow().y val size = coordinates.size From 5cb4b1d8b4c76fdfcdaa1ebad332a7111d79c883 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Sun, 10 May 2026 19:55:56 +0800 Subject: [PATCH 20/27] feat: custom background fill --- app/build.gradle.kts | 1 + .../modules/CustomBoundsCompatModule.kt | 309 +++++++++++++++++- .../bounds/CustomBoundsCompatConfigCodec.kt | 27 ++ .../config/CustomBoundsCompatManagerScreen.kt | 181 +++++++++- app/src/main/res/values-zh-rCN/strings.xml | 12 + app/src/main/res/values/strings.xml | 12 + 6 files changed, 536 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f1b9c13..0fd8dad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,6 +44,7 @@ android { versionName = gropify.project.app.versionName versionCode = gitVersionCode testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField("String", "BUILD_CHANNEL", "\"$buildSuffix\"") } signingConfigs { diff --git a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt index e47794f..03b39e5 100644 --- a/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt +++ b/app/src/main/java/hk/uwu/reareye/hook/scopes/system/modules/CustomBoundsCompatModule.kt @@ -1,14 +1,19 @@ package hk.uwu.reareye.hook.scopes.system.modules +import android.content.Context +import android.content.pm.ActivityInfo import android.content.res.Configuration +import android.graphics.Color import android.graphics.Point import android.graphics.Rect import com.highcapable.kavaref.KavaRef.Companion.asResolver import com.highcapable.kavaref.KavaRef.Companion.resolve import com.highcapable.yukihookapi.hook.entity.YukiBaseHooker import com.highcapable.yukihookapi.hook.log.YLog +import hk.uwu.reareye.BuildConfig import hk.uwu.reareye.repository.bounds.CustomBoundsCompatAppConfig import hk.uwu.reareye.repository.bounds.CustomBoundsCompatConfigCodec +import hk.uwu.reareye.repository.bounds.CustomBoundsFillMode import hk.uwu.reareye.repository.bounds.CustomBoundsMode import hk.uwu.reareye.ui.config.ConfigKeys import kotlin.math.max @@ -19,13 +24,21 @@ class CustomBoundsCompatModule : YukiBaseHooker() { override fun onHook() { loadSystem { val activityRecordImplClass = runCatching { - "com.android.server.wm.ActivityRecordImpl".toClass().resolve() + "com.android.server.wm.ActivityRecordImpl".toClass() + }.getOrElse { + YLog.warn("ActivityRecordImpl class not found, skip custom rear bounds hook") + return@loadSystem + } + val activityRecordImplRef = runCatching { + activityRecordImplClass.resolve() }.getOrElse { YLog.warn("ActivityRecordImpl not found, skip custom rear bounds hook") return@loadSystem } - activityRecordImplClass.firstMethod { + val themeColorUtilsClass = "com.android.server.wm.ThemeColorUtils".toClass() + + activityRecordImplRef.firstMethod { name = "resolveOverrideConfiguration" parameterCount = 2 }.hook().after { @@ -96,15 +109,292 @@ class CustomBoundsCompatModule : YukiBaseHooker() { densityDpi = config.densityDpi.takeIf { it > 0 } ?: parentConfig.densityDpi, rotation = config.rotationDegrees, ) + @Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions") + prepareFlipSplashColor( + activityRecordImpl = instance, + activityRecord = activityRecord, + bounds = compatBounds, + moreDebug = moreDebug, + collectDetails = moreDebug && SHOULD_LOG_DETAILS, + themeColorUtilsClass = themeColorUtilsClass + ) + applyTaskFillColor( + activityRecordImpl = instance, + activityRecord = activityRecord, + config = config, + moreDebug = moreDebug, + ) if (moreDebug) { YLog.debug( - "[$TAG] apply package=$packageName displayId=$displayId parent=$parentBounds bounds=$compatBounds mode=${config.mode} ratio=${config.aspectRatio} insets=${config.insetLeft},${config.insetTop},${config.insetRight},${config.insetBottom} gravity=${config.gravity} scale=${config.scale} dpi=${config.densityDpi} rotation=${config.rotationDegrees}" + "[$TAG] apply package=$packageName displayId=$displayId parent=$parentBounds bounds=$compatBounds mode=${config.mode} ratio=${config.aspectRatio} insets=${config.insetLeft},${config.insetTop},${config.insetRight},${config.insetBottom} gravity=${config.gravity} scale=${config.scale} dpi=${config.densityDpi} rotation=${config.rotationDegrees} fill=${config.fillEnabled}/${config.fillMode}" ) } } } } + private fun prepareFlipSplashColor( + activityRecordImpl: Any?, + activityRecord: Any?, + bounds: Rect, + moreDebug: Boolean, + collectDetails: Boolean, + themeColorUtilsClass: Class<*>? + ) { + val activityInfo = activityRecord.field("info") ?: return + val theme = activityRecord.call("getTheme") + ?: activityInfo.themeResource + val splashColor = computeSplashColorDirect( + activityRecordImpl = activityRecordImpl, + activityRecord = activityRecord, + activityInfo = activityInfo, + theme = theme, + collectDetails = collectDetails, + themeColorUtilsClass = themeColorUtilsClass + ) + if (moreDebug) { + val detailsSuffix = if (collectDetails && splashColor.details != null) { + " details=${splashColor.details}" + } else { + "" + } + YLog.debug( + "[$TAG] prepare flip splash color source=${splashColor.source} package=${activityInfo.packageName} " + + "theme=$theme color=${splashColor.color?.toArgbHex() ?: "null"} bounds=$bounds$detailsSuffix" + ) + } + } + + private fun applyTaskFillColor( + activityRecordImpl: Any?, + activityRecord: Any?, + config: CustomBoundsCompatAppConfig, + moreDebug: Boolean, + ) { + val task = activityRecord.call("getTask") + ?: activityRecord.call("getParent") + ?: return + val surfaceControl = task.field("mSurfaceControl") + ?: task.call("getSurfaceControl") + ?: return + val transaction = task.call("getSyncTransaction") + ?: activityRecord.call("getSyncTransaction") + ?: return + + if (!config.fillEnabled) { + unsetTaskSurfaceColor(transaction, surfaceControl) + requestTraversal(activityRecord) + if (moreDebug) { + YLog.debug("[$TAG] unset fill package=${config.packageName}") + } + return + } + + val fillColor = resolveFillColor( + activityRecordImpl = activityRecordImpl, + config = config, + ) ?: run { + if (moreDebug) { + YLog.debug( + "[$TAG] skip fill package=${config.packageName} reason=no_flip_color " + + describeFlipColorState(activityRecordImpl, activityRecord) + ) + } + return + } + setTaskSurfaceColor(transaction, surfaceControl, fillColor) + requestTraversal(activityRecord) + if (moreDebug) { + YLog.debug( + "[$TAG] set fill package=${config.packageName} mode=${config.fillMode} color=${fillColor.toArgbHex()}" + ) + } + } + + private fun resolveFillColor( + activityRecordImpl: Any?, + config: CustomBoundsCompatAppConfig, + ): Int? { + return when (config.fillMode) { + CustomBoundsFillMode.CUSTOM -> config.fillColorArgb.takeIf(::isUsableFillColor) + CustomBoundsFillMode.AUTO -> resolveBySystemFlipLogic(activityRecordImpl) + } + } + + private fun resolveBySystemFlipLogic(activityRecordImpl: Any?): Int? = runCatching { + activityRecordImpl ?: return@runCatching null + activityRecordImpl.field("mSplashBgColor")?.takeIf(::isUsableFillColor) + }.getOrNull() + + private fun computeSplashColorDirect( + activityRecordImpl: Any?, + activityRecord: Any?, + activityInfo: ActivityInfo, + theme: Int, + collectDetails: Boolean, + themeColorUtilsClass: Class<*>? + ): SplashColorResult { + val details = if (collectDetails) mutableListOf() else null + fun detailsText(): String? = details?.joinToString(";") + val systemContext = activityRecord.systemContext() + ?: activityRecordImpl.systemContext() + ?: return SplashColorResult(null, "none", detailsText()) + val packageContext = createPackageContextForActivity(systemContext, activityInfo) + ?: return SplashColorResult(null, "none", detailsText()) + val resolvedTheme = when { + theme != 0 -> theme + activityInfo.themeResource != 0 -> activityInfo.themeResource + else -> android.R.style.Theme_DeviceDefault_DayNight + } + packageContext.setTheme(resolvedTheme) + details?.add("resolvedTheme=$resolvedTheme") + + val miuiCandidate = resolveThemeColorByMiuiUtils( + context = packageContext, + packageName = activityInfo.packageName, + activityRecordImpl = activityRecordImpl, + themeColorUtilsClass = themeColorUtilsClass, + ) + if (miuiCandidate != null) { + details?.add("${miuiCandidate.source}=${miuiCandidate.color.toArgbHex()}") + if (isUsableFillColor(miuiCandidate.color)) { + return SplashColorResult(miuiCandidate.color, miuiCandidate.source, detailsText()) + } + } + + return SplashColorResult(null, "none", detailsText()) + } + + private data class SplashColorResult( + val color: Int?, + val source: String, + val details: String?, + ) + + private data class ColorCandidate( + val color: Int, + val source: String, + ) + + private fun resolveThemeColorByMiuiUtils( + context: Context, + packageName: String, + activityRecordImpl: Any?, + themeColorUtilsClass: Class<*>?, + ): ColorCandidate? = runCatching { + themeColorUtilsClass ?: return@runCatching null + val attrsClass = $$"com.android.server.wm.ThemeColorUtils$SplashScreenWindowAttrs".toClass( + themeColorUtilsClass.classLoader + ) + val attrs = attrsClass.getDeclaredConstructor() + .apply { isAccessible = true } + .newInstance() + themeColorUtilsClass.getDeclaredMethod("getWindowAttrs", Context::class.java, attrsClass) + .apply { isAccessible = true } + .invoke(null, context, attrs) + val color = themeColorUtilsClass.getDeclaredMethod( + "peekWindowBGColor", + Context::class.java, + attrsClass + ) + .apply { isAccessible = true } + .invoke(null, context, attrs) as? Int + ?: return@runCatching null + if (isUsableFillColor(color)) { + activityRecordImpl.setField("mSplashBgColor", color) + setThemeColorCache(themeColorUtilsClass, packageName, color) + } + ColorCandidate(color, "miuiThemeColorUtils") + }.getOrNull() + + private fun createPackageContextForActivity( + systemContext: Context, + activityInfo: ActivityInfo, + ): Context? { + return runCatching { + val userHandleClass = "android.os.UserHandle".toClass() + val userId = + userHandleClass.getDeclaredMethod("getUserId", Int::class.javaPrimitiveType) + .apply { isAccessible = true } + .invoke(null, activityInfo.applicationInfo.uid) as Int + val userHandle = userHandleClass.getDeclaredMethod("of", Int::class.javaPrimitiveType) + .apply { isAccessible = true } + .invoke(null, userId) + systemContext.javaClass.getMethod( + "createPackageContextAsUser", + String::class.java, + Int::class.javaPrimitiveType, + userHandleClass, + ).invoke( + systemContext, + activityInfo.packageName, + Context.CONTEXT_RESTRICTED, + userHandle + ) as? Context + }.getOrElse { + runCatching { + systemContext.createPackageContext( + activityInfo.packageName, + Context.CONTEXT_RESTRICTED + ) + }.getOrNull() + } + } + + private fun setThemeColorCache( + themeColorUtilsClass: Class<*>, + packageName: String, + color: Int, + ) { + runCatching { + @Suppress("UNCHECKED_CAST") + val cache = themeColorUtilsClass.getDeclaredField("mAppColorCache") + .apply { isAccessible = true } + .get(null) as? MutableMap + cache?.put(packageName, color) + } + } + + private fun describeFlipColorState(activityRecordImpl: Any?, activityRecord: Any?): String { + val splashBgColor = activityRecordImpl.field("mSplashBgColor") + val flipCutoutColor = activityRecordImpl.field("mFlipCutoutColor") + val taskDescription = activityRecord.field("taskDescription") + val navigationBarColor = taskDescription.call("getNavigationBarColor") + val systemContext = activityRecord.systemContext() + val nightMode = systemContext?.resources?.configuration?.isNightModeActive + return "splash=${splashBgColor?.toArgbHex() ?: "null"} " + + "flip=${flipCutoutColor?.toArgbHex() ?: "null"} " + + "taskDescription=${taskDescription != null} " + + "nav=${navigationBarColor?.toArgbHex() ?: "null"} " + + "night=$nightMode" + } + + private fun setTaskSurfaceColor(transaction: Any?, surfaceControl: Any?, colorInt: Int) { + val color = Color.valueOf(colorInt) + transaction.call( + "setColor", + surfaceControl, + floatArrayOf(color.red(), color.green(), color.blue()), + ) + } + + private fun unsetTaskSurfaceColor(transaction: Any?, surfaceControl: Any?) { + transaction.call("unsetColor", surfaceControl) + } + + private fun requestTraversal(activityRecord: Any?) { + activityRecord + .field("mWmService") + ?.field("mWindowPlacerLocked") + ?.call("requestTraversal") + } + + private fun isUsableFillColor(color: Int): Boolean = + color != 0 && (color ushr 24) != 0 + + private fun Int.toArgbHex(): String = + "#" + Integer.toHexString(this).padStart(8, '0').uppercase() + private fun computeInsetsBounds(parentBounds: Rect, config: CustomBoundsCompatAppConfig): Rect { val left = parentBounds.left + config.insetLeft val top = parentBounds.top + config.insetTop @@ -238,6 +528,16 @@ class CustomBoundsCompatModule : YukiBaseHooker() { this?.asResolver()?.firstField { this.name = name }?.get() }.getOrNull() + private fun Any?.setField(name: String, value: Any?) { + runCatching { + this?.asResolver()?.firstField { this.name = name }?.set(value) + } + } + + private fun Any?.systemContext(): Context? = + field("mAtmService")?.field("mContext") + ?: field("mWmService")?.field("mContext") + private fun Any?.call(name: String, vararg args: Any?): T? = runCatching { this?.asResolver()?.firstMethod { this.name = name @@ -272,5 +572,8 @@ class CustomBoundsCompatModule : YukiBaseHooker() { const val RIGHT = 5 const val TOP = 48 const val BOTTOM = 80 + + @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants") + const val SHOULD_LOG_DETAILS = BuildConfig.BUILD_CHANNEL == "dev" } } diff --git a/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt b/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt index 2bb4dc1..e58ff7a 100644 --- a/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt +++ b/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt @@ -16,6 +16,9 @@ data class CustomBoundsCompatAppConfig( val insetTop: Int = 0, val insetRight: Int = 0, val insetBottom: Int = 0, + val fillEnabled: Boolean = false, + val fillMode: CustomBoundsFillMode = CustomBoundsFillMode.AUTO, + val fillColorArgb: Int = 0, ) enum class CustomBoundsMode { @@ -35,6 +38,21 @@ enum class CustomBoundsMode { } } +enum class CustomBoundsFillMode { + AUTO, + CUSTOM; + + companion object { + fun fromString(value: String?): CustomBoundsFillMode? { + return when (value?.trim()?.lowercase()) { + "auto" -> AUTO + "custom" -> CUSTOM + else -> null + } + } + } +} + object CustomBoundsCompatConfigCodec { const val EMPTY_ARRAY = "[]" const val DEFAULT_GRAVITY = 17 @@ -74,6 +92,9 @@ object CustomBoundsCompatConfigCodec { aspectRatio > 0f -> CustomBoundsMode.CUSTOM_RATIO else -> CustomBoundsMode.AUTO_RATIO } + val fillMode = CustomBoundsFillMode.fromString( + if (obj.has("fillMode")) obj.optString("fillMode") else null + ) ?: CustomBoundsFillMode.AUTO out += CustomBoundsCompatAppConfig( packageName = packageName, enabled = obj.optBoolean("enabled", true), @@ -95,6 +116,9 @@ object CustomBoundsCompatConfigCodec { insetTop = obj.optInt("insetTop", 0).coerceAtLeast(0), insetRight = obj.optInt("insetRight", 0).coerceAtLeast(0), insetBottom = obj.optInt("insetBottom", 0).coerceAtLeast(0), + fillEnabled = obj.optBoolean("fillEnabled", false), + fillMode = fillMode, + fillColorArgb = obj.optInt("fillColorArgb", 0), ) } return out @@ -122,6 +146,9 @@ object CustomBoundsCompatConfigCodec { .put("insetTop", item.insetTop) .put("insetRight", item.insetRight) .put("insetBottom", item.insetBottom) + .put("fillEnabled", item.fillEnabled) + .put("fillMode", item.fillMode.name.lowercase()) + .put("fillColorArgb", item.fillColorArgb) ) } }.toString() diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt index f966ac4..517221f 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,8 +14,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add @@ -32,15 +36,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.core.graphics.toColorInt import hk.uwu.reareye.R import hk.uwu.reareye.repository.bounds.CustomBoundsCompatAppConfig import hk.uwu.reareye.repository.bounds.CustomBoundsCompatConfigCodec +import hk.uwu.reareye.repository.bounds.CustomBoundsFillMode import hk.uwu.reareye.repository.bounds.CustomBoundsMode import hk.uwu.reareye.ui.components.DialogFormColumn import hk.uwu.reareye.ui.components.OverlayDialog @@ -65,6 +72,7 @@ import kotlinx.coroutines.withContext import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.ColorPalette import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator @@ -78,6 +86,7 @@ import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Delete import top.yukonga.miuix.kmp.preference.SwitchPreference import top.yukonga.miuix.kmp.preference.WindowSpinnerPreference +import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.miuixShape import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic @@ -144,6 +153,10 @@ fun CustomBoundsCompatManagerScreen( var draftInsetTop by remember { mutableStateOf("") } var draftInsetRight by remember { mutableStateOf("") } var draftInsetBottom by remember { mutableStateOf("") } + var draftFillEnabled by remember { mutableStateOf(false) } + var draftFillMode by remember { mutableIntStateOf(0) } + var draftFillColor by remember { mutableStateOf("#FF000000") } + var draftFillColorError by remember { mutableStateOf(false) } val appSelectorItem = remember { ConfigItem( @@ -203,6 +216,13 @@ fun CustomBoundsCompatManagerScreen( draftInsetTop = item.insetTop.takeIf { it > 0 }?.toString().orEmpty() draftInsetRight = item.insetRight.takeIf { it > 0 }?.toString().orEmpty() draftInsetBottom = item.insetBottom.takeIf { it > 0 }?.toString().orEmpty() + draftFillEnabled = item.fillEnabled + draftFillMode = when (item.fillMode) { + CustomBoundsFillMode.AUTO -> 0 + CustomBoundsFillMode.CUSTOM -> 1 + } + draftFillColor = formatColorInt(item.fillColorArgb.takeIf { it != 0 } ?: 0xFF000000.toInt()) + draftFillColorError = false showDialog = true } @@ -222,13 +242,21 @@ fun CustomBoundsCompatManagerScreen( val insetTop = draftInsetTop.trim().ifBlank { "0" }.toIntOrNull() val insetRight = draftInsetRight.trim().ifBlank { "0" }.toIntOrNull() val insetBottom = draftInsetBottom.trim().ifBlank { "0" }.toIntOrNull() + val fillMode = when (draftFillMode) { + 1 -> CustomBoundsFillMode.CUSTOM + else -> CustomBoundsFillMode.AUTO + } + val parsedFillColor = parseColorInt(draftFillColor) + draftFillColorError = + draftFillEnabled && fillMode == CustomBoundsFillMode.CUSTOM && parsedFillColor == null if (scale == null || scale <= 0f || densityDpi == null || densityDpi < 0 || gravityOptions.none { it.value == gravity } || CustomBoundsCompatConfigCodec.normalizeRotation(rotation) != rotation || insetLeft == null || insetLeft < 0 || insetTop == null || insetTop < 0 || insetRight == null || insetRight < 0 || insetBottom == null || insetBottom < 0 || - (mode == CustomBoundsMode.CUSTOM_RATIO && (aspectRatio == null || aspectRatio <= 0f)) + (mode == CustomBoundsMode.CUSTOM_RATIO && (aspectRatio == null || aspectRatio <= 0f)) || + (draftFillEnabled && fillMode == CustomBoundsFillMode.CUSTOM && parsedFillColor == null) ) { Toast.makeText( context, @@ -251,6 +279,13 @@ fun CustomBoundsCompatManagerScreen( insetTop = insetTop, insetRight = insetRight, insetBottom = insetBottom, + fillEnabled = draftFillEnabled, + fillMode = fillMode, + fillColorArgb = if (draftFillEnabled && fillMode == CustomBoundsFillMode.CUSTOM) { + parsedFillColor ?: 0 + } else { + 0 + }, ) val index = configs.indexOfFirst { it.packageName == packageName } if (index >= 0) { @@ -494,9 +529,8 @@ fun CustomBoundsCompatManagerScreen( ) } if (draftMode == 0) { - Text( + CustomBoundsInfoText( text = stringResource(R.string.custom_bounds_auto_ratio_hint), - modifier = Modifier.fillMaxWidth(), ) } CustomBoundsDropdownPreference( @@ -527,6 +561,37 @@ fun CustomBoundsCompatManagerScreen( selectedValue = draftRotationDegrees, onSelected = { draftRotationDegrees = it }, ) + SwitchPreference( + title = stringResource(R.string.custom_bounds_fill_enable), + checked = draftFillEnabled, + onCheckedChange = { draftFillEnabled = it }, + ) + if (draftFillEnabled) { + CustomBoundsDropdownPreference( + title = stringResource(R.string.custom_bounds_fill_mode), + description = stringResource(R.string.custom_bounds_fill_mode_help), + options = listOf( + CustomBoundsOption(0, R.string.custom_bounds_fill_mode_auto), + CustomBoundsOption(1, R.string.custom_bounds_fill_mode_custom), + ), + selectedValue = draftFillMode, + onSelected = { draftFillMode = it }, + ) + if (draftFillMode == 0) { + CustomBoundsInfoText( + text = stringResource(R.string.custom_bounds_fill_auto_hint), + ) + } else { + CustomBoundsColorEditor( + value = draftFillColor, + onValueChange = { + draftFillColor = it + draftFillColorError = parseColorInt(it) == null + }, + error = draftFillColorError, + ) + } + } Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -580,6 +645,101 @@ private fun CustomBoundsDropdownPreference( ) } +@Composable +private fun CustomBoundsInfoText(text: String) { + Text( + text = text, + modifier = Modifier + .fillMaxWidth() + .clip(miuixShape(16.dp)) + .background(MiuixTheme.colorScheme.secondaryContainer.copy(alpha = 0.42f)) + .border( + width = 0.5.dp, + color = MiuixTheme.colorScheme.outline.copy(alpha = 0.18f), + shape = miuixShape(16.dp), + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) +} + +@Composable +private fun CustomBoundsColorEditor( + value: String, + onValueChange: (String) -> Unit, + error: Boolean, +) { + val parsedColor = remember(value) { parseColorInt(value) } + val composeColor = parsedColor?.let(::Color) ?: Color.White + + fun commitColor(color: Color) { + onValueChange(formatColorInt(color.toArgb())) + } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.custom_bounds_fill_palette), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + ColorPalette( + color = composeColor, + onColorChanged = { commitColor(it) }, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = stringResource(R.string.custom_bounds_fill_color_help), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(RoundedCornerShape(999.dp)) + .border( + width = 1.dp, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + shape = RoundedCornerShape(999.dp), + ) + .background(parsedColor?.let(::Color) ?: Color.Transparent), + ) + TextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.weight(1f), + label = stringResource(R.string.custom_bounds_fill_color), + singleLine = true, + ) + } + if (error) { + Text( + text = stringResource(R.string.custom_bounds_fill_color_invalid), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.error, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +private fun parseColorInt(raw: String): Int? { + val normalized = raw.trim() + if (normalized.isBlank()) return null + return runCatching { normalized.toColorInt() }.getOrNull() +} + +private fun formatColorInt(color: Int): String = + String.format(Locale.US, "#%08X", color) + @Composable private fun customBoundsBadges(item: CustomBoundsCompatAppConfig): List { val statePalette = rememberRearAccentBadgePalette( @@ -589,6 +749,7 @@ private fun customBoundsBadges(item: CustomBoundsCompatAppConfig): ListDPI(留空跟随系统) 旋转角度 选择固定的背屏旋转角度,或保持跟随系统。 + 填充空白区域 + 填充模式 + 自动模式会优先使用应用启动/背景颜色;自定义模式使用下方颜色。 + 自动取色 + 自定义颜色 + 自动取应用启动/背景颜色,取不到时回退到系统明暗色。 + 调色板 + 输入十六进制颜色,例如 #FF000000 或 #000000。 + 填充颜色 + 颜色值无效 请填写有效的界限配置 界限配置已保存 比例 %1$s · 缩放 %2$s · 位置 %3$d @@ -554,4 +564,6 @@ 旋转 %1$s 已启用 已停用 + 未填充 + 填充 %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c929560..959f2d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -528,6 +528,16 @@ Widget: %2$s · Priority: %3$d DPI (empty follows system) Rotation Choose a fixed rear-screen rotation, or keep following the system. + Fill empty area + Fill mode + Auto uses the app splash/background color when available; custom uses the color below. + Auto color + Custom color + Automatically samples the app splash/background color, falling back to the system light/dark color. + Palette + Enter a hex color such as #FF000000 or #000000. + Fill color + Invalid color value Please enter valid bounds settings Bounds settings saved Ratio %1$s · Scale %2$s · Gravity %3$d @@ -554,4 +564,6 @@ Widget: %2$s · Priority: %3$d Rotation %1$s Enabled Disabled + Fill off + Fill %1$s From 28294a592c2d4a6cc73fbd3719796fcf2b9bec96 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Sun, 10 May 2026 22:40:37 +0800 Subject: [PATCH 21/27] chore: improved license details visibility --- .../hk/uwu/reareye/ui/components/RearBadge.kt | 65 ++- .../utils/other/AboutLibrariesTools.kt | 371 +++++++++++++++--- 2 files changed, 373 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt b/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt index f10bb55..37063ad 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/RearBadge.kt @@ -1,15 +1,19 @@ package hk.uwu.reareye.ui.components +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.luminance import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -27,6 +31,7 @@ data class RearBadgeItem( val text: String, val emphasized: Boolean = false, val palette: RearBadgePalette? = null, + val onClick: (() -> Unit)? = null, ) @Composable @@ -138,6 +143,7 @@ fun RearBadgePill( fun RearBadgeGroup( badges: List, modifier: Modifier = Modifier, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, ) { if (badges.isEmpty()) return @@ -146,9 +152,20 @@ fun RearBadgeGroup( modifier = modifier, content = { badges.forEach { badge -> + val badgeModifier = if (badge.onClick != null) { + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + role = Role.Button, + onClick = badge.onClick, + ) + } else { + Modifier + } RearBadgePill( text = badge.text, emphasized = badge.emphasized, + modifier = badgeModifier, palette = badge.palette, singleLine = true, ) @@ -165,24 +182,42 @@ fun RearBadgeGroup( val placeableIndex: Int, ) + data class BadgeLine( + val positions: List, + val width: Int, + val height: Int, + ) + val maxRowWidth = constraints.maxWidth - val positions = mutableListOf() + val lines = mutableListOf() + val currentLinePositions = mutableListOf() var currentX = 0 var currentY = 0 var currentLineHeight = 0 var maxUsedWidth = 0 + fun commitLine() { + if (currentLinePositions.isEmpty()) return + lines += BadgeLine( + positions = currentLinePositions.toList(), + width = currentX, + height = currentLineHeight, + ) + maxUsedWidth = maxOf(maxUsedWidth, currentX) + currentY += currentLineHeight + spacingPx + currentLinePositions.clear() + currentX = 0 + currentLineHeight = 0 + } + placeables.forEachIndexed { index, placeable -> val proposedX = if (currentX == 0) 0 else currentX + spacingPx if (currentX > 0 && proposedX + placeable.width > maxRowWidth) { - maxUsedWidth = maxOf(maxUsedWidth, currentX) - currentY += currentLineHeight + spacingPx - currentX = 0 - currentLineHeight = 0 + commitLine() } val placeX = if (currentX == 0) 0 else currentX + spacingPx - positions += BadgePosition( + currentLinePositions += BadgePosition( x = placeX, y = currentY, placeableIndex = index, @@ -190,15 +225,25 @@ fun RearBadgeGroup( currentX = placeX + placeable.width currentLineHeight = maxOf(currentLineHeight, placeable.height) } + commitLine() - maxUsedWidth = maxOf(maxUsedWidth, currentX) - val contentHeight = if (placeables.isEmpty()) 0 else currentY + currentLineHeight + val contentHeight = if (placeables.isEmpty()) 0 else currentY - spacingPx val layoutWidth = maxUsedWidth.coerceIn(constraints.minWidth, constraints.maxWidth) val layoutHeight = contentHeight.coerceIn(constraints.minHeight, constraints.maxHeight) layout(layoutWidth, layoutHeight) { - positions.forEach { position -> - placeables[position.placeableIndex].placeRelative(position.x, position.y) + lines.forEach { line -> + val lineOffsetX = when (horizontalAlignment) { + Alignment.CenterHorizontally -> (layoutWidth - line.width) / 2 + Alignment.End -> layoutWidth - line.width + else -> 0 + }.coerceAtLeast(0) + line.positions.forEach { position -> + placeables[position.placeableIndex].placeRelative( + position.x + lineOffsetX, + position.y + ) + } } } } diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt index 70956dd..46f6b8d 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt @@ -2,14 +2,14 @@ package hk.uwu.reareye.utils.other import android.content.Context import android.content.Intent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -21,17 +21,22 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.net.toUri import hk.uwu.reareye.R -import hk.uwu.reareye.ui.components.OverlayDialog -import hk.uwu.reareye.ui.components.RearBadgePill +import hk.uwu.reareye.ui.components.RearBadgeGroup +import hk.uwu.reareye.ui.components.RearBadgeItem import hk.uwu.reareye.ui.components.card.SuperCard import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -43,6 +48,7 @@ import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Link +import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet import top.yukonga.miuix.kmp.theme.MiuixTheme @Serializable @@ -86,6 +92,9 @@ private val json = Json { ignoreUnknownKeys = true } +private const val LicenseBadgeOverflowText = "..." +private const val LicenseCreditsAnimationDurationMillis = 220 + fun loadLibraries(context: Context): AboutLibraries { val inputStream = context.resources.openRawResource(R.raw.aboutlibraries) @@ -100,7 +109,8 @@ fun loadLibraries(context: Context): AboutLibraries { fun LibraryItem(lib: Library, licenses: Map) { val context = LocalContext.current val hasLink = lib.website != null - var showLicense by remember { mutableStateOf(null) } + var showLicenses by remember { mutableStateOf>(emptyList()) } + var expandLicensesInline by remember { mutableStateOf(false) } Card(modifier = Modifier.fillMaxWidth()) { SuperCard( @@ -116,7 +126,7 @@ fun LibraryItem(lib: Library, licenses: Map) { disabledContentColor = Color.Transparent, ), onClick = { - lib.website?.let { targetLink -> + lib.website.let { targetLink -> context.startActivity( Intent( Intent.ACTION_VIEW, @@ -144,51 +154,47 @@ fun LibraryItem(lib: Library, licenses: Map) { color = MiuixTheme.colorScheme.outline.copy(alpha = 0.5f) ) } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - if (lib.developers.isNotEmpty()) { - Box(modifier = Modifier.weight(1f)) { - LibraryDevelopersText(lib.developers) - } - } - if (lib.licenses.isNotEmpty()) { - Column ( - verticalArrangement = Arrangement.spacedBy(4.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - lib.licenses.forEach { licenseId -> - val license = licenses[licenseId] - RearBadgePill( - modifier = Modifier.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { - license?.let { showLicense = it } - }, - text = license?.name?.take(15) ?: if (licenseId.length > 15) licenseId.take(15) + "\u2026" else licenseId, - emphasized = false - ) - } - } - } + val licenseEntries = lib.licenses.map { licenseId -> + licenseId to licenses[licenseId] } - showLicense?.let { lic -> + LibraryCreditsRow( + developers = lib.developers, + licenseBadges = licenseEntries.map { (licenseId, license) -> + RearBadgeItem( + text = licenseBadgeText(licenseId, license), + emphasized = false, + onClick = license?.let { targetLicense -> + { showLicenses = listOf(targetLicense) } + }, + ) + }, + expanded = expandLicensesInline, + onExpand = { expandLicensesInline = true }, + onCollapse = { expandLicensesInline = false }, + ) + if (showLicenses.isNotEmpty()) { val primaryColor = MiuixTheme.colorScheme.primary - OverlayDialog( + val dialogTitle = remember(showLicenses) { + showLicenses.joinToString(" / ") { it.name } + } + OverlayBottomSheet( show = true, - title = lic.name, - onDismissRequest = { showLicense = null } + title = dialogTitle, + onDismissRequest = { showLicenses = emptyList() } ) { - // 准备原始文本 - val rawText = lic.content ?: lic.url + val rawText = remember(showLicenses) { + showLicenses.joinToString("\n\n") { license -> + buildString { + append(license.name) + append("\n\n") + append(license.content ?: license.url) + } + } + } - // 自动识别链接并构建 AnnotatedString val annotatedText = remember(rawText) { buildAnnotatedString { append(rawText) - // 匹配 http/https 链接的正则表达式 val urlPattern = Regex("(https?://[\\w-]+(\\.[\\w-]+)+(/\\S*)?)") urlPattern.findAll(rawText).forEach { match -> addLink( @@ -196,8 +202,8 @@ fun LibraryItem(lib: Library, licenses: Map) { url = match.value, styles = TextLinkStyles( SpanStyle( - color = primaryColor , - fontWeight = FontWeight.Bold + color = primaryColor, + fontWeight = FontWeight.Bold, ) ) ), @@ -210,9 +216,11 @@ fun LibraryItem(lib: Library, licenses: Map) { Text( modifier = Modifier - .padding(vertical = 8.dp) + .fillMaxWidth() + .heightIn(max = 560.dp) + .padding(top = 8.dp) .verticalScroll(rememberScrollState()), - text = annotatedText, // 传入处理好的 AnnotatedString + text = annotatedText, color = MiuixTheme.colorScheme.onSurfaceVariantSummary ) } @@ -232,9 +240,254 @@ private fun buildLibrarySummary(lib: Library): String? { return parts.takeIf { it.isNotEmpty() }?.joinToString("\n") } +private fun licenseBadgeText(licenseId: String, license: License?): String { + return license?.name ?: licenseId +} + +@Composable +private fun LibraryCreditsRow( + developers: List, + licenseBadges: List, + expanded: Boolean, + onExpand: () -> Unit, + onCollapse: () -> Unit, +) { + if (developers.isEmpty() && licenseBadges.isEmpty()) return + + val badgeSpacing = 12.dp + val authorWordSpacing = 4.dp + val authorText = buildLibraryDevelopersText(developers) + val authorWords = remember(authorText) { splitAnnotatedWords(authorText) } + val overflowBadges = remember(licenseBadges, onExpand) { + if (licenseBadges.isEmpty()) { + emptyList() + } else { + listOf( + RearBadgeItem( + text = LicenseBadgeOverflowText, + emphasized = false, + onClick = onExpand, + ) + ) + } + } + val collapsedBySpaceState = remember { mutableStateOf(false) } + val shouldExpand = expanded && collapsedBySpaceState.value + val authorTargetAlpha = if (shouldExpand) 0f else 1f + val authorAlpha by animateFloatAsState( + targetValue = authorTargetAlpha, + animationSpec = tween(durationMillis = LicenseCreditsAnimationDurationMillis), + label = "license-author-alpha", + ) + val fullBadgeAlpha by animateFloatAsState( + targetValue = if (!collapsedBySpaceState.value || shouldExpand) 1f else 0f, + animationSpec = tween(durationMillis = LicenseCreditsAnimationDurationMillis), + label = "license-full-badge-alpha", + ) + val overflowBadgeAlpha by animateFloatAsState( + targetValue = if (collapsedBySpaceState.value && !shouldExpand) 1f else 0f, + animationSpec = tween(durationMillis = LicenseCreditsAnimationDurationMillis), + label = "license-overflow-badge-alpha", + ) + val rowHeightProgress by animateFloatAsState( + targetValue = if (shouldExpand) 1f else 0f, + animationSpec = tween(durationMillis = LicenseCreditsAnimationDurationMillis), + label = "license-row-height-progress", + ) + + Layout( + modifier = Modifier + .fillMaxWidth() + .clickable( + enabled = shouldExpand, + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onCollapse, + ), + content = { + authorWords.forEach { word -> + LibraryDevelopersText( + text = word, + modifier = Modifier.graphicsLayer { alpha = authorAlpha }, + ) + } + Box(contentAlignment = Alignment.CenterEnd) { + if (licenseBadges.isNotEmpty()) { + RearBadgeGroup( + badges = licenseBadges, + modifier = Modifier.graphicsLayer { alpha = fullBadgeAlpha }, + horizontalAlignment = Alignment.End, + ) + } + } + Box(contentAlignment = Alignment.CenterEnd) { + if (overflowBadges.isNotEmpty()) { + RearBadgeGroup( + badges = overflowBadges, + modifier = Modifier.graphicsLayer { alpha = overflowBadgeAlpha }, + horizontalAlignment = Alignment.End, + ) + } + } + }, + ) { measurables, constraints -> + val maxWidth = constraints.maxWidth + val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val wordMeasurables = measurables.take(authorWords.size) + val fullBadgeMeasurable = measurables[authorWords.size] + val overflowBadgeMeasurable = measurables[authorWords.size + 1] + val hasAuthor = authorWords.isNotEmpty() + val hasLicenses = licenseBadges.isNotEmpty() + val requestedBadgeSpacingPx = if (hasAuthor && hasLicenses) badgeSpacing.roundToPx() else 0 + val wordSpacingPx = authorWordSpacing.roundToPx() + + val overflowBadgePlaceable = overflowBadgeMeasurable.measure(looseConstraints) + val fullBadgePlaceable = + fullBadgeMeasurable.measure(looseConstraints.copy(maxWidth = maxWidth)) + val fullBadgeFitsOneLine = + !hasLicenses || fullBadgePlaceable.height <= overflowBadgePlaceable.height + val firstWordWidth = wordMeasurables.firstOrNull() + ?.maxIntrinsicWidth(constraints.maxHeight) + ?.coerceAtMost(maxWidth) + ?: 0 + val fullBadgeSpacingPx = requestedBadgeSpacingPx.coerceAtMost( + (maxWidth - fullBadgePlaceable.width).coerceAtLeast(0) + ) + val firstLineWidthWithFullBadge = + (maxWidth - fullBadgePlaceable.width - fullBadgeSpacingPx).coerceAtLeast(0) + val collapsedBySpace = hasLicenses && hasAuthor && + (!fullBadgeFitsOneLine || firstWordWidth > firstLineWidthWithFullBadge) + collapsedBySpaceState.value = collapsedBySpace + val collapsedBadgePlaceable = + if (collapsedBySpace) overflowBadgePlaceable else fullBadgePlaceable + val collapsedBadgeSpacingPx = requestedBadgeSpacingPx.coerceAtMost( + (maxWidth - collapsedBadgePlaceable.width).coerceAtLeast(0) + ) + val collapsedFirstLineMaxWidth = if (hasLicenses && hasAuthor) { + (maxWidth - collapsedBadgePlaceable.width - collapsedBadgeSpacingPx).coerceAtLeast(0) + } else { + maxWidth + } + + val expandedBadgePlaceable = fullBadgePlaceable + val expandedBadgeHeight = fullBadgePlaceable.height + + data class WordPlacement( + val placeableIndex: Int, + val x: Int, + val y: Int, + ) + + fun measureAuthor(firstLineMaxWidth: Int): Pair, List> { + val wordPlaceables = mutableListOf() + val wordPlacements = mutableListOf() + var lineIndex = 0 + var currentX = 0 + var currentY = 0 + var currentLineHeight = if (hasLicenses) collapsedBadgePlaceable.height else 0 + + fun currentLineMaxWidth(): Int { + return if (lineIndex == 0) firstLineMaxWidth else maxWidth + } + + fun moveToNextLine() { + currentY += currentLineHeight + lineIndex += 1 + currentX = 0 + currentLineHeight = 0 + } + + wordMeasurables.forEach { measurable -> + val intrinsicWidth = + measurable.maxIntrinsicWidth(constraints.maxHeight).coerceAtMost(maxWidth) + var lineMaxWidth = currentLineMaxWidth() + var wordX = if (currentX == 0) 0 else currentX + wordSpacingPx + + if (currentX > 0 && wordX + intrinsicWidth > lineMaxWidth) { + moveToNextLine() + lineMaxWidth = currentLineMaxWidth() + wordX = 0 + } + + val wordMaxWidth = (lineMaxWidth - wordX).takeIf { it > 0 } ?: lineMaxWidth + val placeable = measurable.measure( + looseConstraints.copy(maxWidth = intrinsicWidth.coerceAtMost(wordMaxWidth)) + ) + wordPlacements += WordPlacement( + placeableIndex = wordPlaceables.size, + x = wordX, + y = currentY, + ) + wordPlaceables += placeable + currentX = wordX + placeable.width + currentLineHeight = maxOf(currentLineHeight, placeable.height) + } + return wordPlaceables to wordPlacements + } + + val (wordPlaceables, wordPlacements) = measureAuthor(collapsedFirstLineMaxWidth) + val authorHeight = if (hasAuthor && wordPlacements.isNotEmpty()) { + wordPlacements.maxOf { placement -> + placement.y + wordPlaceables[placement.placeableIndex].height + } + } else { + 0 + } + val collapsedContentHeight = + maxOf(authorHeight, if (hasLicenses) collapsedBadgePlaceable.height else 0) + val expandedContentHeight = if (hasLicenses) expandedBadgeHeight else collapsedContentHeight + val layoutHeight = (collapsedContentHeight + + ((expandedContentHeight - collapsedContentHeight) * rowHeightProgress).toInt()) + .coerceIn(constraints.minHeight, constraints.maxHeight) + val collapsedBadgeY = ((layoutHeight - collapsedBadgePlaceable.height) / 2).coerceAtLeast(0) + val fullBadgeY = ((layoutHeight - expandedBadgePlaceable.height) / 2).coerceAtLeast(0) + + layout(maxWidth, layoutHeight) { + wordPlacements.forEach { placement -> + val placeable = wordPlaceables[placement.placeableIndex] + placeable.placeRelative(placement.x, placement.y) + } + if (hasLicenses) { + if (!collapsedBySpace || shouldExpand || fullBadgeAlpha > 0.01f) { + expandedBadgePlaceable.placeRelative( + maxWidth - expandedBadgePlaceable.width, + fullBadgeY + ) + } + if (collapsedBySpace && (!shouldExpand || overflowBadgeAlpha > 0.01f)) { + collapsedBadgePlaceable.placeRelative( + maxWidth - collapsedBadgePlaceable.width, + collapsedBadgeY + ) + } + } + } + } +} + +private fun splitAnnotatedWords(text: AnnotatedString): List { + if (text.isEmpty()) return emptyList() + + val words = mutableListOf() + var index = 0 + while (index < text.length) { + while (index < text.length && text[index] == ' ') { + index += 1 + } + val start = index + while (index < text.length && text[index] != ' ') { + index += 1 + } + if (start < index) { + words += text.subSequence(start, index) + } + } + return words +} + @Composable -private fun LibraryDevelopersText(developers: List) { - val annotated = buildAnnotatedString { +private fun buildLibraryDevelopersText(developers: List): AnnotatedString { + return buildAnnotatedString { developers.forEachIndexed { index, developer -> val name = developer.name ?: "Unknown" val url = developer.organisationUrl @@ -248,7 +501,6 @@ private fun LibraryDevelopersText(developers: List) { styles = TextLinkStyles( SpanStyle( color = MiuixTheme.colorScheme.primary, - fontWeight = FontWeight.Bold, ) ) ), @@ -264,5 +516,18 @@ private fun LibraryDevelopersText(developers: List) { } } } - Text(text = annotated) -} \ No newline at end of file +} + +@Composable +private fun LibraryDevelopersText( + text: AnnotatedString, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier, + text = text, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + ) +} From 968ffb54ccc24871777ad6c25e4f54c52e52435e Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Sun, 10 May 2026 22:56:49 +0800 Subject: [PATCH 22/27] chore: improved store scroll down effect --- .../uwu/reareye/ui/screen/RearStoreScreen.kt | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt index 1cbfebf..2760bab 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt @@ -121,7 +121,6 @@ import hk.uwu.reareye.ui.components.card.ModuleStyleDeleteAction import hk.uwu.reareye.ui.components.card.SuperCard import hk.uwu.reareye.ui.components.config.WallpaperMetadataFields import hk.uwu.reareye.ui.components.motion.ArtRevealItem -import hk.uwu.reareye.ui.components.motion.ArtStaggeredReveal import hk.uwu.reareye.ui.components.rememberRearAccentBadgePalette import hk.uwu.reareye.ui.components.webview.ScrollWebView import hk.uwu.reareye.ui.config.ConfigKeys @@ -1060,19 +1059,13 @@ private fun RearStoreRootContent( RearStoreMessageCard(text = stringResource(R.string.rear_store_search_empty)) } } else { - itemsIndexed(items, key = { _, item -> item.id }) { index, item -> - ArtStaggeredReveal( - visible = true, - revealKey = item.id + searchQuery, - delayMillis = (24 + index * 18).coerceAtMost(160), - ) { - RearStoreListCard( - item = item, - installedWidget = installedWidgets[item.id], - updateAvailable = item.hasAvailableUpdate(installedWidgets), - onClick = { onOpenDetail(item.id) }, - ) - } + itemsIndexed(items, key = { _, item -> item.id }) { _, item -> + RearStoreListCard( + item = item, + installedWidget = installedWidgets[item.id], + updateAvailable = item.hasAvailableUpdate(installedWidgets), + onClick = { onOpenDetail(item.id) }, + ) } } } From aedbc1ef6166e9b98363e8f6e7cbda4c6a1e3a36 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 11 May 2026 13:35:37 +0800 Subject: [PATCH 23/27] feat: nav quick access chore: allow negative value for insets chore: qodana code quality chore: upgrade dependencies --- .github/workflows/qodana_code_quality.yml | 39 + app/build.gradle.kts | 6 + .../java/hk/uwu/reareye/ui/MainActivity.kt | 51 + .../config/CustomBoundsCompatManagerScreen.kt | 42 +- .../navigation/FloatingBottomBar.kt | 53 +- .../navigation/NavigationQuickActionConfig.kt | 47 + .../navigation/RearNavigationBar.kt | 1507 ++++++++++++++++- .../java/hk/uwu/reareye/ui/config/Config.kt | 1 + .../hk/uwu/reareye/ui/config/ModuleConfig.kt | 1 + .../hk/uwu/reareye/ui/screen/ConfigScreen.kt | 163 +- app/src/main/res/values-zh-rCN/strings.xml | 24 +- app/src/main/res/values/strings.xml | 24 +- gradle/libs.versions.toml | 19 +- qodana.yaml | 48 + 14 files changed, 1872 insertions(+), 153 deletions(-) create mode 100644 .github/workflows/qodana_code_quality.yml create mode 100644 app/src/main/java/hk/uwu/reareye/ui/components/navigation/NavigationQuickActionConfig.kt create mode 100644 qodana.yaml diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000..406c121 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,39 @@ +#-------------------------------------------------------------------------------# +# Discover additional configuration options in our documentation # +# https://www.jetbrains.com/help/qodana/github.html # +#-------------------------------------------------------------------------------# + +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: + - dev + +jobs: + qodana: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2025.3 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} + with: + # When pr-mode is set to true, Qodana analyzes only the files that have been changed + pr-mode: false + use-caches: true + post-pr-comment: true + use-annotations: true + # Upload Qodana results (SARIF, other artifacts, logs) as an artifact to the job + upload-result: true + # quick-fixes available in Ultimate and Ultimate Plus plans + push-fixes: 'none' \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0fd8dad..1b1999f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -195,6 +195,12 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.compose.icons.material.symbols.outlined.cmp) + implementation(libs.compose.icons.material.symbols.rounded.cmp) + implementation(libs.compose.icons.material.symbols.sharp.cmp) + implementation(libs.compose.icons.material.symbols.outlined.filled.cmp) + implementation(libs.compose.icons.material.symbols.rounded.filled.cmp) + implementation(libs.compose.icons.material.symbols.sharp.filled.cmp) implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.miuix.ui) diff --git a/app/src/main/java/hk/uwu/reareye/ui/MainActivity.kt b/app/src/main/java/hk/uwu/reareye/ui/MainActivity.kt index de7b417..b9b5bf1 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/MainActivity.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/MainActivity.kt @@ -5,6 +5,9 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing @@ -34,8 +37,12 @@ import androidx.core.content.ContextCompat import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import hk.uwu.reareye.ui.components.motion.ArtVisibilityMotion +import hk.uwu.reareye.ui.components.navigation.NavigationQuickTarget import hk.uwu.reareye.ui.components.navigation.RearNavigationBar +import hk.uwu.reareye.ui.components.navigation.encodeNavigationQuickActionIds +import hk.uwu.reareye.ui.components.navigation.parseNavigationQuickActionIds import hk.uwu.reareye.ui.config.ConfigKeys +import hk.uwu.reareye.ui.config.ConfigType import hk.uwu.reareye.ui.config.ModuleNavigationBarMode import hk.uwu.reareye.ui.config.ModuleSettingsController import hk.uwu.reareye.ui.config.PrefsManager.Companion.getPrefsManager @@ -100,6 +107,17 @@ class MainActivity : ComponentActivity() { var currentScreen by remember { mutableStateOf("home") } var navBarVisible by remember { mutableStateOf(false) } var configInAppListMode by remember { mutableStateOf(false) } + var pendingConfigQuickManagerTarget by remember { + mutableStateOf(null) + } + var pendingQuickActionTransition by remember { mutableStateOf(false) } + var navigationQuickActionIds by remember { + mutableStateOf( + parseNavigationQuickActionIds( + prefsManager.getString(ConfigKeys.MODULE_NAVIGATION_QUICK_ACTIONS) + ) + ) + } LaunchedEffect(Unit) { navBarVisible = true @@ -140,6 +158,14 @@ class MainActivity : ComponentActivity() { targetState = currentScreen, contentKey = { it }, transitionSpec = { + if (pendingQuickActionTransition && targetState == "config") { + pendingQuickActionTransition = false + return@AnimatedContent ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None, + ) + } + val initialIndex = MainScreenOrder.indexOf(initialState).coerceAtLeast(0) val targetIndex = @@ -184,6 +210,10 @@ class MainActivity : ComponentActivity() { "config" -> ConfigScreen( bottomInnerPadding = stableBottomInset, + quickManagerTarget = pendingConfigQuickManagerTarget, + onQuickManagerTargetHandled = { + pendingConfigQuickManagerTarget = null + }, onAppListModeChange = { configInAppListMode = it }, @@ -237,7 +267,28 @@ class MainActivity : ComponentActivity() { navigationBarMode = navigationBarMode, backdrop = backdrop, shadowVisibilityProgress = navShadowProgress, + quickActionIds = navigationQuickActionIds, + onQuickActionIdsChanged = { nextIds -> + val normalizedIds = parseNavigationQuickActionIds( + encodeNavigationQuickActionIds(nextIds) + ) + navigationQuickActionIds = normalizedIds + prefsManager.putString( + ConfigKeys.MODULE_NAVIGATION_QUICK_ACTIONS, + encodeNavigationQuickActionIds(normalizedIds), + ) + }, onScreenSelected = { currentScreen = it }, + onQuickActionSelected = { target -> + when (target) { + is NavigationQuickTarget.ConfigManager -> { + pendingConfigQuickManagerTarget = target.managerType + pendingQuickActionTransition = true + configInAppListMode = true + currentScreen = "config" + } + } + }, ) } } diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt index 517221f..22a2bed 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt @@ -253,8 +253,8 @@ fun CustomBoundsCompatManagerScreen( if (scale == null || scale <= 0f || densityDpi == null || densityDpi < 0 || gravityOptions.none { it.value == gravity } || CustomBoundsCompatConfigCodec.normalizeRotation(rotation) != rotation || - insetLeft == null || insetLeft < 0 || insetTop == null || insetTop < 0 || - insetRight == null || insetRight < 0 || insetBottom == null || insetBottom < 0 || + insetLeft == null || /*insetLeft < 0 ||*/ insetTop == null || /*insetTop < 0 ||*/ + insetRight == null || /*insetRight < 0 ||*/ insetBottom == null ||/* insetBottom < 0 ||*/ (mode == CustomBoundsMode.CUSTOM_RATIO && (aspectRatio == null || aspectRatio <= 0f)) || (draftFillEnabled && fillMode == CustomBoundsFillMode.CUSTOM && parsedFillColor == null) ) { @@ -767,22 +767,28 @@ private fun customBoundsBadges(item: CustomBoundsCompatAppConfig): List RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_ratio, + formatFloat(item.aspectRatio), + ), + palette = ratioPalette, + ) + + CustomBoundsMode.EXACT_INSETS -> RearBadgeItem( + text = stringResource( + R.string.custom_bounds_badge_insets, + "${item.insetLeft},${item.insetTop},${item.insetRight},${item.insetBottom}", + ), + palette = ratioPalette, + ) + + else -> RearBadgeItem( + text = stringResource(R.string.custom_bounds_auto_ratio_hint), + palette = ratioPalette, + ) + }, RearBadgeItem( text = stringResource( R.string.custom_bounds_badge_scale, diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/FloatingBottomBar.kt b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/FloatingBottomBar.kt index 3bc838d..19da4cd 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/FloatingBottomBar.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/FloatingBottomBar.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.EaseOut import androidx.compose.animation.core.spring import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -64,23 +65,20 @@ import kotlin.math.abs import kotlin.math.sign val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } } +val LocalFloatingBottomBarDuplicateLayer = staticCompositionLocalOf { false } @Composable fun RowScope.FloatingBottomBarItem( onClick: () -> Unit, modifier: Modifier = Modifier, + quickGestureModifier: Modifier = Modifier, + popup: @Composable (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) { val scale = LocalFloatingBottomBarTabScale.current - Column( + val isDuplicateLayer = LocalFloatingBottomBarDuplicateLayer.current + Box( modifier - .clip(ContinuousCapsule) - .clickable( - interactionSource = null, - indication = null, - role = Role.Tab, - onClick = onClick, - ) .fillMaxHeight() .weight(1f) .graphicsLayer { @@ -88,10 +86,28 @@ fun RowScope.FloatingBottomBarItem( scaleX = scaleValue scaleY = scaleValue }, - verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - content = content, - ) + contentAlignment = Alignment.Center, + ) { + Column( + Modifier + .matchParentSize() + .clip(ContinuousCapsule) + .then(quickGestureModifier) + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + role = Role.Tab, + onClick = onClick, + onLongClick = null, + ), + verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + content = content, + ) + if (!isDuplicateLayer) { + popup?.invoke() + } + } } @Composable @@ -103,6 +119,9 @@ fun FloatingBottomBar( tabsCount: Int, isBlurEnabled: Boolean = true, shadowVisibilityProgress: Float = 1f, + selectedOverlayModifier: Modifier = Modifier, + renderDuplicateContentLayer: Boolean = true, + quickGestureModifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit, ) { val isInLightTheme = MiuixTheme.colorScheme.surface.luminance() >= 0.5f @@ -215,7 +234,9 @@ fun FloatingBottomBar( null } Box( - modifier = modifier.width(IntrinsicSize.Min), + modifier = modifier + .width(IntrinsicSize.Min) + .then(quickGestureModifier), contentAlignment = Alignment.CenterStart, ) { Row( @@ -267,11 +288,12 @@ fun FloatingBottomBar( content = content, ) - if (isBlurEnabled) { + if (isBlurEnabled && renderDuplicateContentLayer) { CompositionLocalProvider( LocalFloatingBottomBarTabScale provides { lerp(1f, 1.2f, dampedDragAnimation.pressProgress) - } + }, + LocalFloatingBottomBarDuplicateLayer provides true, ) { Row( Modifier @@ -319,6 +341,7 @@ fun FloatingBottomBar( } } .then(interactiveHighlight?.gestureModifier ?: Modifier) + .then(selectedOverlayModifier) .then(dampedDragAnimation.modifier) .drawBackdrop( backdrop = if (isBlurEnabled) { diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/NavigationQuickActionConfig.kt b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/NavigationQuickActionConfig.kt new file mode 100644 index 0000000..85a0295 --- /dev/null +++ b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/NavigationQuickActionConfig.kt @@ -0,0 +1,47 @@ +package hk.uwu.reareye.ui.components.navigation + +const val MaxNavigationQuickActions = 5 + +const val NavigationQuickActionComponentManagerId = "config_manager_business" +const val NavigationQuickActionCardManagerId = "config_manager_card" +const val NavigationQuickActionWallpaperManagerId = "config_manager_wallpaper" +const val NavigationQuickActionSceneRouteManagerId = "config_manager_scene_route" +const val NavigationQuickActionBusinessExtraManagerId = "config_manager_business_extra" +const val NavigationQuickActionBoundsManagerId = "config_manager_bounds" +const val NavigationQuickActionLyricsManagerId = "config_manager_lyrics" + +val DefaultNavigationQuickActionIds = listOf( + NavigationQuickActionComponentManagerId, + NavigationQuickActionCardManagerId, +) + +val AvailableNavigationQuickActionIds = listOf( + NavigationQuickActionComponentManagerId, + NavigationQuickActionCardManagerId, + NavigationQuickActionWallpaperManagerId, + NavigationQuickActionSceneRouteManagerId, + NavigationQuickActionBusinessExtraManagerId, + NavigationQuickActionBoundsManagerId, + NavigationQuickActionLyricsManagerId, +) + +fun parseNavigationQuickActionIds(value: String): List { + val availableLookup = AvailableNavigationQuickActionIds.toSet() + val parsed = value + .split('|') + .map { it.trim() } + .filter { it.isNotEmpty() && it in availableLookup } + .distinct() + .take(MaxNavigationQuickActions) + + return parsed.ifEmpty { DefaultNavigationQuickActionIds } +} + +fun encodeNavigationQuickActionIds(ids: List): String { + val availableLookup = AvailableNavigationQuickActionIds.toSet() + return ids + .filter { it in availableLookup } + .distinct() + .take(MaxNavigationQuickActions) + .joinToString("|") +} \ No newline at end of file diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt index eb1e16f..282c28e 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/navigation/RearNavigationBar.kt @@ -1,50 +1,163 @@ package hk.uwu.reareye.ui.components.navigation +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.BorderStyle import androidx.compose.material.icons.rounded.CloudDownload import androidx.compose.material.icons.rounded.Cottage +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.Extension import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.LibraryMusic +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material.icons.rounded.Route import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material.icons.rounded.Wallpaper +import androidx.compose.material.icons.rounded.Widgets import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import androidx.compose.ui.zIndex +import com.composables.icons.materialsymbols.MaterialSymbols +import com.composables.icons.materialsymbols.rounded.Stacks import com.kyant.backdrop.Backdrop +import com.kyant.backdrop.backdrops.layerBackdrop +import com.kyant.backdrop.backdrops.rememberLayerBackdrop +import com.kyant.backdrop.drawBackdrop +import com.kyant.backdrop.effects.blur +import com.kyant.backdrop.effects.lens +import com.kyant.backdrop.effects.vibrancy +import com.kyant.backdrop.highlight.Highlight +import com.kyant.backdrop.shadow.InnerShadow +import com.kyant.backdrop.shadow.Shadow +import com.kyant.capsule.ContinuousCapsule import hk.uwu.reareye.R +import hk.uwu.reareye.ui.config.ConfigType import hk.uwu.reareye.ui.config.ModuleNavigationBarMode import hk.uwu.reareye.utils.BlurredBar import hk.uwu.reareye.utils.blend.rememberBlurBackdrop +import top.yukonga.miuix.kmp.basic.Button +import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.NavigationBar import top.yukonga.miuix.kmp.basic.NavigationBarDisplayMode -import top.yukonga.miuix.kmp.basic.NavigationBarItem import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.overlay.OverlayBottomSheet import top.yukonga.miuix.kmp.theme.MiuixTheme +sealed interface NavigationQuickTarget { + @Immutable + data class ConfigManager(val managerType: ConfigType.ManagerType) : NavigationQuickTarget +} + +@Immutable +private data class NavigationQuickAction( + val id: String, + val label: String, + val icon: ImageVector, + val target: NavigationQuickTarget, +) + @Immutable private data class NavigationDestination( val route: String, val label: String, val icon: ImageVector, + val quickActions: List = emptyList(), +) + +@Immutable +private data class NavigationQuickActionStyle( + val containerColor: Color, + val contentColor: Color, + val borderColor: Color, + val hoveredContainerColor: Color, + val selectedContainerColor: Color, + val selectedBorderColor: Color, + val shadowAlpha: Float, + val shadowElevation: Dp, + val usesGlass: Boolean, ) private const val HOME_ROUTE = "home" private const val STORE_ROUTE = "store" private const val CONFIG_ROUTE = "config" private const val ABOUT_ROUTE = "about" +private const val EDIT_QUICK_ACTIONS_KEY = "__edit_quick_actions__" +private const val QUICK_ACTION_BUTTON_WIDTH_DP = 176 +private const val QUICK_ACTION_HALF_LONG_PRESS_MIN_MS = 180L @Composable fun RearNavigationBar( @@ -53,13 +166,32 @@ fun RearNavigationBar( navigationBarMode: ModuleNavigationBarMode, backdrop: Backdrop, shadowVisibilityProgress: Float = 1f, + quickActionIds: List = DefaultNavigationQuickActionIds, + onQuickActionIdsChanged: (List) -> Unit = {}, onScreenSelected: (String) -> Unit, + onQuickActionSelected: (NavigationQuickTarget) -> Unit = {}, ) { val homeLabel = stringResource(R.string.home_navigation) val storeLabel = stringResource(R.string.store_navigation) val configLabel = stringResource(R.string.configuration_navigation) val aboutLabel = stringResource(R.string.about_navigation) - val items = remember(homeLabel, storeLabel, configLabel, aboutLabel) { + val editLabel = stringResource(R.string.navigation_quick_edit) + val allQuickActions = rememberNavigationQuickActions() + val quickActionLookup = remember(allQuickActions) { allQuickActions.associateBy { it.id } } + val enabledQuickActions = remember(quickActionIds, allQuickActions) { + quickActionIds + .mapNotNull { quickActionLookup[it] } + .distinctBy { it.id } + .take(MaxNavigationQuickActions) + } + + val items = remember( + homeLabel, + storeLabel, + configLabel, + aboutLabel, + enabledQuickActions, + ) { listOf( NavigationDestination( route = HOME_ROUTE, @@ -75,6 +207,7 @@ fun RearNavigationBar( route = CONFIG_ROUTE, label = configLabel, icon = Icons.Rounded.Settings, + quickActions = enabledQuickActions, ), NavigationDestination( route = ABOUT_ROUTE, @@ -83,20 +216,78 @@ fun RearNavigationBar( ), ) } + val quickMenuController = remember { NavigationQuickMenuController() } + var expandedQuickActionsRoute by remember { mutableStateOf(null) } + var showQuickActionEditor by remember { mutableStateOf(false) } + + fun openQuickActions( + item: NavigationDestination, + anchorCenterX: Float, + anchorTop: Float, + density: Float + ) { + if (item.quickActions.isNotEmpty()) { + quickMenuController.begin(anchorCenterX, anchorTop, density) + expandedQuickActionsRoute = item.route + } + } + + fun closeQuickActions() { + expandedQuickActionsRoute = null + quickMenuController.reset() + } + + fun openEditor() { + closeQuickActions() + showQuickActionEditor = true + } + + fun selectQuickAction(target: NavigationQuickTarget) { + quickMenuController.reset() + onQuickActionSelected(target) + expandedQuickActionsRoute = null + } + + NavigationQuickActionEditor( + show = showQuickActionEditor, + selectedIds = quickActionIds, + allActions = allQuickActions, + onDismissRequest = { showQuickActionEditor = false }, + onSave = { nextIds -> + onQuickActionIdsChanged(nextIds) + showQuickActionEditor = false + }, + ) if (navigationBarMode == ModuleNavigationBarMode.NORMAL || navigationBarMode == ModuleNavigationBarMode.SEMI_TRANSPARENT) { val bar: @Composable () -> Unit = { NavigationBar( modifier = modifier, - color = if (navigationBarMode == ModuleNavigationBarMode.NORMAL) MiuixTheme.colorScheme.surface else Color.Transparent, + color = if (navigationBarMode == ModuleNavigationBarMode.NORMAL) { + MiuixTheme.colorScheme.surface + } else { + Color.Transparent + }, mode = NavigationBarDisplayMode.IconAndText, ) { items.forEach { item -> - NavigationBarItem( + RearNavigationBarItem( + item = item, selected = currentScreen == item.route, - onClick = { onScreenSelected(item.route) }, - icon = item.icon, - label = item.label, + expanded = expandedQuickActionsRoute == item.route, + navigationBarMode = navigationBarMode, + editLabel = editLabel, + quickMenuController = quickMenuController, + onClick = { + closeQuickActions() + onScreenSelected(item.route) + }, + onOpenQuickActions = { anchorCenterX, anchorTop, density -> + openQuickActions(item, anchorCenterX, anchorTop, density) + }, + onDismissQuickActions = ::closeQuickActions, + onOpenEditor = ::openEditor, + onQuickActionSelected = ::selectQuickAction, ) } } @@ -112,6 +303,20 @@ fun RearNavigationBar( val enableGlass = navigationBarMode == ModuleNavigationBarMode.FLOATING_GLASS val selectedIndex = items.indexOfFirst { it.route == currentScreen }.coerceAtLeast(0) + val selectedItem = items.getOrNull(selectedIndex) + val selectedOverlayQuickGesture = selectedItem?.let { item -> + Modifier.navigationQuickActionDragGesture( + enabled = item.quickActions.isNotEmpty(), + actions = item.quickActions, + quickMenuController = quickMenuController, + onOpenQuickActions = { anchorCenterX, anchorTop, density -> + openQuickActions(item, anchorCenterX, anchorTop, density) + }, + onDismissQuickActions = ::closeQuickActions, + onOpenEditor = ::openEditor, + onQuickActionSelected = ::selectQuickAction, + ) + } ?: Modifier FloatingBottomBar( modifier = modifier @@ -125,15 +330,47 @@ fun RearNavigationBar( .calculateBottomPadding() ), selectedIndex = selectedIndex, - onSelected = { onScreenSelected(items[it].route) }, + onSelected = { + closeQuickActions() + onScreenSelected(items[it].route) + }, backdrop = backdrop, tabsCount = items.size, isBlurEnabled = enableGlass, shadowVisibilityProgress = shadowVisibilityProgress, + selectedOverlayModifier = Modifier, + renderDuplicateContentLayer = true, + quickGestureModifier = selectedOverlayQuickGesture, ) { items.forEachIndexed { index, item -> FloatingBottomBarItem( - onClick = { onScreenSelected(items[index].route) }, + onClick = { + closeQuickActions() + onScreenSelected(items[index].route) + }, + quickGestureModifier = Modifier.navigationQuickActionDragGesture( + enabled = item.quickActions.isNotEmpty(), + actions = item.quickActions, + quickMenuController = quickMenuController, + onOpenQuickActions = { anchorCenterX, anchorTop, density -> + openQuickActions(item, anchorCenterX, anchorTop, density) + }, + onDismissQuickActions = ::closeQuickActions, + onOpenEditor = ::openEditor, + onQuickActionSelected = ::selectQuickAction, + ), + popup = { + NavigationQuickActionsPopup( + expanded = expandedQuickActionsRoute == item.route, + actions = item.quickActions, + navigationBarMode = navigationBarMode, + editLabel = editLabel, + quickMenuController = quickMenuController, + onDismissRequest = ::closeQuickActions, + onOpenEditor = ::openEditor, + onActionSelected = ::selectQuickAction, + ) + }, modifier = Modifier.defaultMinSize(minWidth = 76.dp), ) { Icon( @@ -154,3 +391,1255 @@ fun RearNavigationBar( } } } + +@Composable +private fun rememberNavigationQuickActions(): List { + val componentManagerLabel = stringResource(R.string.navigation_quick_component_manager) + val cardManagerLabel = stringResource(R.string.navigation_quick_card_manager) + val wallpaperManagerLabel = stringResource(R.string.rear_wallpaper_manager) + val sceneRouteManagerLabel = stringResource(R.string.rear_widget_scene_route_manager) + val businessExtraManagerLabel = stringResource(R.string.rear_widget_business_extra_manager) + val boundsManagerLabel = stringResource(R.string.custom_bounds_compat_manager) + val lyricsManagerLabel = stringResource(R.string.navigation_quick_lyrics_manager) + + return remember( + componentManagerLabel, + cardManagerLabel, + wallpaperManagerLabel, + sceneRouteManagerLabel, + businessExtraManagerLabel, + boundsManagerLabel, + lyricsManagerLabel, + ) { + listOf( + NavigationQuickAction( + id = NavigationQuickActionComponentManagerId, + label = componentManagerLabel, + icon = Icons.Rounded.Widgets, + target = NavigationQuickTarget.ConfigManager(ConfigType.ManagerType.BUSINESS), + ), + NavigationQuickAction( + id = NavigationQuickActionCardManagerId, + label = cardManagerLabel, + icon = MaterialSymbols.Rounded.Stacks, + target = NavigationQuickTarget.ConfigManager(ConfigType.ManagerType.CARD), + ), + NavigationQuickAction( + id = NavigationQuickActionWallpaperManagerId, + label = wallpaperManagerLabel, + icon = Icons.Rounded.Wallpaper, + target = NavigationQuickTarget.ConfigManager(ConfigType.ManagerType.REAR_WALLPAPER), + ), + NavigationQuickAction( + id = NavigationQuickActionSceneRouteManagerId, + label = sceneRouteManagerLabel, + icon = Icons.Rounded.Route, + target = NavigationQuickTarget.ConfigManager(ConfigType.ManagerType.SCENE_ROUTE), + ), + NavigationQuickAction( + id = NavigationQuickActionBusinessExtraManagerId, + label = businessExtraManagerLabel, + icon = Icons.Rounded.Extension, + target = NavigationQuickTarget.ConfigManager(ConfigType.ManagerType.BUSINESS_EXTRA), + ), + NavigationQuickAction( + id = NavigationQuickActionBoundsManagerId, + label = boundsManagerLabel, + icon = Icons.Rounded.BorderStyle, + target = NavigationQuickTarget.ConfigManager(ConfigType.ManagerType.BOUNDS), + ), + NavigationQuickAction( + id = NavigationQuickActionLyricsManagerId, + label = lyricsManagerLabel, + icon = Icons.Rounded.LibraryMusic, + target = NavigationQuickTarget.ConfigManager(ConfigType.ManagerType.LYRICS), + ), + ) + } +} + +@Composable +private fun RowScope.RearNavigationBarItem( + item: NavigationDestination, + selected: Boolean, + expanded: Boolean, + navigationBarMode: ModuleNavigationBarMode, + editLabel: String, + quickMenuController: NavigationQuickMenuController, + onClick: () -> Unit, + onOpenQuickActions: (anchorCenterX: Float, anchorTop: Float, density: Float) -> Unit, + onDismissQuickActions: () -> Unit, + onOpenEditor: () -> Unit, + onQuickActionSelected: (NavigationQuickTarget) -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val onSurfaceContainerColor = MiuixTheme.colorScheme.onSurfaceContainer + val tint = when { + isPressed -> if (selected) { + onSurfaceContainerColor.copy(alpha = 0.5f) + } else { + onSurfaceContainerColor.copy(alpha = 0.6f) + } + + selected -> onSurfaceContainerColor + else -> onSurfaceContainerColor.copy(alpha = 0.4f) + } + val fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal + + Box( + modifier = Modifier + .height(64.dp) + .weight(1f), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .navigationQuickActionDragGesture( + enabled = item.quickActions.isNotEmpty(), + actions = item.quickActions, + quickMenuController = quickMenuController, + onOpenQuickActions = onOpenQuickActions, + onDismissQuickActions = onDismissQuickActions, + onOpenEditor = onOpenEditor, + onQuickActionSelected = onQuickActionSelected, + ) + .combinedClickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + onLongClick = null, + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + Icon( + imageVector = item.icon, + contentDescription = item.label, + tint = tint, + modifier = Modifier + .padding(top = 8.dp) + .size(26.dp), + ) + Text( + modifier = Modifier.padding(bottom = 8.dp), + text = item.label, + color = tint, + fontSize = 12.sp, + fontWeight = fontWeight, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + ) + } + + NavigationQuickActionsPopup( + expanded = expanded, + actions = item.quickActions, + navigationBarMode = navigationBarMode, + editLabel = editLabel, + quickMenuController = quickMenuController, + onDismissRequest = onDismissQuickActions, + onOpenEditor = onOpenEditor, + onActionSelected = onQuickActionSelected, + ) + } +} + +@Composable +private fun NavigationQuickActionsPopup( + expanded: Boolean, + actions: List, + navigationBarMode: ModuleNavigationBarMode, + editLabel: String, + quickMenuController: NavigationQuickMenuController, + onDismissRequest: () -> Unit, + onOpenEditor: () -> Unit, + onActionSelected: (NavigationQuickTarget) -> Unit, +) { + val density = LocalDensity.current + val menuBackdrop = rememberLayerBackdrop() + val positionProvider = remember(density) { + NavigationQuickActionsPopupPositionProvider( + spacingPx = with(density) { 10.dp.roundToPx() }, + windowPaddingPx = with(density) { 12.dp.roundToPx() }, + ) + } + val progress = remember { Animatable(0f) } + var popupVisible by remember { mutableStateOf(false) } + + LaunchedEffect(expanded, actions) { + if (expanded && actions.isNotEmpty()) { + popupVisible = true + progress.animateTo( + targetValue = 1f, + animationSpec = tween(220, easing = FastOutSlowInEasing), + ) + } else if (popupVisible) { + progress.animateTo( + targetValue = 0f, + animationSpec = tween(170, easing = FastOutSlowInEasing), + ) + popupVisible = false + } + } + + if ((!expanded && !popupVisible) || actions.isEmpty()) return + + Popup( + popupPositionProvider = positionProvider, + onDismissRequest = onDismissRequest, + properties = PopupProperties( + focusable = false, + dismissOnBackPress = true, + dismissOnClickOutside = true, + ), + ) { + val progressValue = progress.value + Box( + modifier = Modifier + .padding(2.dp) + .graphicsLayer { + alpha = progressValue + val scale = 0.92f + 0.08f * progressValue + scaleX = scale + scaleY = scale + translationY = with(density) { (1f - progressValue) * 8.dp.toPx() } + }, + contentAlignment = Alignment.TopCenter, + ) { + val buttonWidth = QUICK_ACTION_BUTTON_WIDTH_DP.dp + val showEditTarget = + quickMenuController.showEditTarget || quickMenuController.hoveredKey == EDIT_QUICK_ACTIONS_KEY + val editTargetProgress by animateFloatAsState( + targetValue = if (showEditTarget) 1f else 0f, + animationSpec = tween( + if (showEditTarget) 150 else 180, + easing = FastOutSlowInEasing + ), + label = "QuickActionEditTargetProgress", + ) + + NavigationQuickActionsBackdropLayer( + actionsCount = actions.size, + editTargetProgress = editTargetProgress, + width = buttonWidth, + modifier = Modifier.layerBackdrop(menuBackdrop), + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .width(buttonWidth) + .height(40.dp), + contentAlignment = Alignment.Center, + ) { + if (editTargetProgress > 0.01f) { + NavigationQuickActionButton( + label = editLabel, + icon = Icons.Rounded.Edit, + navigationBarMode = navigationBarMode, + backdrop = menuBackdrop, + width = buttonWidth, + hovered = quickMenuController.hoveredKey == EDIT_QUICK_ACTIONS_KEY, + isEditTarget = true, + modifier = Modifier.graphicsLayer { + alpha = editTargetProgress + val appearScale = 0.94f + 0.06f * editTargetProgress + scaleX = appearScale + scaleY = appearScale + }, + onClick = onOpenEditor, + onPositioned = { _ -> }, + ) + } + } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + actions.forEachIndexed { index, action -> + NavigationQuickActionButton( + label = action.label, + icon = action.icon, + navigationBarMode = navigationBarMode, + backdrop = menuBackdrop, + width = buttonWidth, + hovered = quickMenuController.hoveredKey == action.id, + delayProgress = ((progressValue - index * 0.05f).coerceIn(0f, 1f)), + onClick = { onActionSelected(action.target) }, + onPositioned = { _ -> }, + ) + } + } + } + } + } +} + +@Composable +private fun NavigationQuickActionsBackdropLayer( + actionsCount: Int, + editTargetProgress: Float, + width: Dp, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .width(width) + .height(40.dp) + .graphicsLayer { alpha = editTargetProgress } + .background( + MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.72f), + ContinuousCapsule + ), + ) + repeat(actionsCount) { + Box( + modifier = Modifier + .width(width) + .height(40.dp) + .background( + MiuixTheme.colorScheme.surfaceContainer.copy(alpha = 0.66f), + ContinuousCapsule + ), + ) + } + } +} + +@Composable +private fun NavigationQuickActionButton( + label: String, + icon: ImageVector, + navigationBarMode: ModuleNavigationBarMode, + backdrop: Backdrop, + width: Dp, + hovered: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + delayProgress: Float = 1f, + isEditTarget: Boolean = false, + onPositioned: (LayoutCoordinates) -> Unit = {}, +) { + val density = LocalDensity.current + val style = rememberNavigationQuickActionStyle(navigationBarMode) + val shouldUseGlass = style.usesGlass + val targetContainerColor = when { + hovered && shouldUseGlass -> style.selectedContainerColor + hovered -> style.hoveredContainerColor + else -> style.containerColor + } + val containerColor by animateColorAsState( + targetValue = targetContainerColor, + animationSpec = tween(160, easing = FastOutSlowInEasing), + label = "QuickActionContainerColor", + ) + val borderColor by animateColorAsState( + targetValue = if (hovered && shouldUseGlass) style.selectedBorderColor else style.borderColor, + animationSpec = tween(160, easing = FastOutSlowInEasing), + label = "QuickActionBorderColor", + ) + val targetScale = when { + hovered && isEditTarget -> 1.12f + hovered -> 1.08f + isEditTarget -> 1.04f + else -> 1f + } + val animatedScale by animateFloatAsState( + targetValue = targetScale, + animationSpec = spring(dampingRatio = 0.72f, stiffness = 420f), + label = "QuickActionScale", + ) + val progressScale = 0.96f + 0.04f * delayProgress + + val baseModifier = modifier + .onGloballyPositioned(onPositioned) + .width(width) + .height(40.dp) + .graphicsLayer { + shape = ContinuousCapsule + clip = true + scaleX = animatedScale * progressScale + scaleY = animatedScale * progressScale + shadowElevation = + with(density) { (style.shadowElevation + if (hovered) 8.dp else 0.dp).toPx() } + ambientShadowColor = Color.Black.copy(alpha = style.shadowAlpha) + spotShadowColor = Color.Black.copy(alpha = style.shadowAlpha) + } + .then( + if (shouldUseGlass) { + Modifier.drawBackdrop( + backdrop = backdrop, + shape = { ContinuousCapsule }, + effects = { + vibrancy() + blur(if (hovered) 10f.dp.toPx() else 8f.dp.toPx()) + lens( + if (hovered) 34f.dp.toPx() else 24f.dp.toPx(), + if (hovered) 38f.dp.toPx() else 26f.dp.toPx(), + ) + }, + highlight = { + Highlight.Default.copy(alpha = if (hovered) 1f else 0.86f) + }, + shadow = { + Shadow.Default.copy( + color = Color.Black.copy(alpha = 0.18f), + alpha = if (hovered) 0.58f else 0.32f, + ) + }, + innerShadow = { + InnerShadow( + radius = if (hovered) 12.dp else 7.dp, + alpha = if (hovered) 0.42f else 0.22f, + ) + }, + onDrawSurface = { + drawRect(containerColor) + drawRect(Color.White.copy(alpha = if (hovered) 0.16f else 0.055f)) + drawRect(Color.Black.copy(alpha = if (hovered) 0.025f else 0.012f)) + }, + ) + } else { + Modifier.background(containerColor, ContinuousCapsule) + } + ) + .border(0.75.dp, borderColor, ContinuousCapsule) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onClick, + ) + + Box( + modifier = baseModifier, + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier.padding(horizontal = 14.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = style.contentColor, + modifier = Modifier.size(19.dp), + ) + AutoShrinkText( + text = label, + color = style.contentColor, + maxFontSize = 12.sp, + minFontSize = 9.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun rememberNavigationQuickActionStyle( + navigationBarMode: ModuleNavigationBarMode, +): NavigationQuickActionStyle { + val isLight = MiuixTheme.colorScheme.surface.luminance() >= 0.5f + val outline = MiuixTheme.colorScheme.dividerLine.copy(alpha = if (isLight) 0.7f else 0.86f) + val primaryTint = MiuixTheme.colorScheme.primary.copy(alpha = if (isLight) 0.14f else 0.2f) + return when (navigationBarMode) { + ModuleNavigationBarMode.NORMAL -> NavigationQuickActionStyle( + containerColor = MiuixTheme.colorScheme.surface, + contentColor = MiuixTheme.colorScheme.onSurface, + borderColor = outline, + hoveredContainerColor = blendColors(MiuixTheme.colorScheme.surface, primaryTint), + selectedContainerColor = blendColors(MiuixTheme.colorScheme.surface, primaryTint), + selectedBorderColor = outline, + shadowAlpha = if (isLight) 0.13f else 0.28f, + shadowElevation = 10.dp, + usesGlass = false, + ) + + ModuleNavigationBarMode.SEMI_TRANSPARENT -> NavigationQuickActionStyle( + containerColor = MiuixTheme.colorScheme.surface.copy(alpha = 0.9f), + contentColor = MiuixTheme.colorScheme.onSurface, + borderColor = outline.copy(alpha = 0.9f), + hoveredContainerColor = blendColors( + MiuixTheme.colorScheme.surface.copy(alpha = 0.94f), + primaryTint + ), + selectedContainerColor = blendColors( + MiuixTheme.colorScheme.surface.copy(alpha = 0.94f), + primaryTint + ), + selectedBorderColor = outline.copy(alpha = 0.9f), + shadowAlpha = if (isLight) 0.16f else 0.3f, + shadowElevation = 12.dp, + usesGlass = false, + ) + + ModuleNavigationBarMode.FLOATING -> NavigationQuickActionStyle( + containerColor = MiuixTheme.colorScheme.surfaceContainer, + contentColor = MiuixTheme.colorScheme.onSurfaceContainer, + borderColor = outline, + hoveredContainerColor = blendColors( + MiuixTheme.colorScheme.surfaceContainer, + primaryTint + ), + selectedContainerColor = blendColors( + MiuixTheme.colorScheme.surfaceContainer, + primaryTint + ), + selectedBorderColor = outline, + shadowAlpha = if (isLight) 0.18f else 0.34f, + shadowElevation = 14.dp, + usesGlass = false, + ) + + ModuleNavigationBarMode.FLOATING_GLASS -> NavigationQuickActionStyle( + containerColor = if (isLight) { + Color.White.copy(alpha = 0.115f) + } else { + Color.White.copy(alpha = 0.065f) + }, + contentColor = MiuixTheme.colorScheme.onSurface, + borderColor = Color.White.copy(alpha = if (isLight) 0.34f else 0.15f), + hoveredContainerColor = if (isLight) { + Color.White.copy(alpha = 0.18f) + } else { + Color.White.copy(alpha = 0.105f) + }, + selectedContainerColor = if (isLight) { + Color.White.copy(alpha = 0.245f) + } else { + Color.White.copy(alpha = 0.145f) + }, + selectedBorderColor = Color.White.copy(alpha = if (isLight) 0.58f else 0.28f), + shadowAlpha = if (isLight) 0.14f else 0.3f, + shadowElevation = 16.dp, + usesGlass = true, + ) + } +} + +private fun blendColors(base: Color, overlay: Color): Color { + val alpha = overlay.alpha.coerceIn(0f, 1f) + return Color( + red = base.red * (1f - alpha) + overlay.red * alpha, + green = base.green * (1f - alpha) + overlay.green * alpha, + blue = base.blue * (1f - alpha) + overlay.blue * alpha, + alpha = base.alpha, + ) +} + +@Composable +private fun AutoShrinkText( + text: String, + color: Color, + maxFontSize: TextUnit, + minFontSize: TextUnit, + modifier: Modifier = Modifier, + fontWeight: FontWeight? = null, + textAlign: TextAlign? = null, +) { + var fontSize by remember(text, maxFontSize) { mutableStateOf(maxFontSize) } + Text( + text = text, + modifier = modifier, + color = color, + fontSize = fontSize, + fontWeight = fontWeight, + textAlign = textAlign, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + onTextLayout = { layoutResult -> + if (layoutResult.hasVisualOverflow && fontSize.value > minFontSize.value) { + fontSize = (fontSize.value - 0.5f).coerceAtLeast(minFontSize.value).sp + } + }, + ) +} + +private class NavigationQuickMenuController { + private var densityScaleFallback = 1f + + var hoveredKey by mutableStateOf(null) + private set + + var showEditTarget by mutableStateOf(false) + private set + + private var anchorCenterX = 0f + private var anchorTop = 0f + + fun begin(anchorCenterX: Float, anchorTop: Float, density: Float) { + densityScaleFallback = density + this.anchorCenterX = anchorCenterX + this.anchorTop = anchorTop + hoveredKey = null + showEditTarget = false + } + + fun reset() { + hoveredKey = null + showEditTarget = false + anchorCenterX = 0f + anchorTop = 0f + } + + fun updateHover(pointer: Offset, actions: List): String? { + val buttonWidth = QUICK_ACTION_BUTTON_WIDTH_DP * densityScaleFallback + val buttonHeight = 40f * densityScaleFallback + val gap = 8f * densityScaleFallback + val menuBottom = anchorTop - 10f * densityScaleFallback + val menuTop = + menuBottom - actions.size * buttonHeight - (actions.size - 1).coerceAtLeast(0) * gap + val xInMenu = + pointer.x in (anchorCenterX - buttonWidth / 2f)..(anchorCenterX + buttonWidth / 2f) + + if (pointer.y <= menuTop + buttonHeight / 2f && xInMenu) { + showEditTarget = true + } + + val actionIndex = ((pointer.y - menuTop) / (buttonHeight + gap)).toInt() + val action = actions.getOrNull(actionIndex) + val actionTop = menuTop + actionIndex * (buttonHeight + gap) + val actionHovered = + action != null && xInMenu && pointer.y in actionTop..(actionTop + buttonHeight) + + if (showEditTarget) { + val editTop = menuTop - gap - buttonHeight + val editBottom = editTop + buttonHeight + if (xInMenu && pointer.y in editTop..editBottom) { + hoveredKey = EDIT_QUICK_ACTIONS_KEY + return hoveredKey + } + if (actionHovered && actionIndex > 0) { + showEditTarget = false + } + } + + hoveredKey = if (actionHovered) { + action.id + } else { + null + } + return hoveredKey + } +} + +private fun Modifier.navigationQuickActionDragGesture( + enabled: Boolean, + actions: List, + quickMenuController: NavigationQuickMenuController, + onOpenQuickActions: (anchorCenterX: Float, anchorTop: Float, density: Float) -> Unit, + onDismissQuickActions: () -> Unit, + onOpenEditor: () -> Unit, + onQuickActionSelected: (NavigationQuickTarget) -> Unit, +): Modifier { + if (!enabled) return this + + var rootOffset = Offset.Zero + return this + .onGloballyPositioned { coordinates -> + rootOffset = coordinates.positionInRoot() + } + .pointerInput(actions) { + awaitEachGesture { + val down = + awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) + val longPressTimeout = maxOf( + QUICK_ACTION_HALF_LONG_PRESS_MIN_MS, + viewConfiguration.longPressTimeoutMillis / 2, + ) + val longPressTriggered = withTimeoutOrNull(longPressTimeout) { + while (true) { + val event = awaitPointerEvent(PointerEventPass.Initial) + val change = event.changes.firstOrNull { it.id == down.id } + ?: return@withTimeoutOrNull false + if (!change.pressed) { + return@withTimeoutOrNull false + } + } + } == null + + if (!longPressTriggered) return@awaitEachGesture + + val anchorCenterX = rootOffset.x + size.width / 2f + val anchorTop = rootOffset.y + onOpenQuickActions(anchorCenterX, anchorTop, density) + var latestRootPosition = rootOffset + down.position + quickMenuController.updateHover(latestRootPosition, actions) + + var released = false + while (!released) { + val event = awaitPointerEvent(PointerEventPass.Initial) + event.changes.forEach { change -> + if (change.id == down.id) { + latestRootPosition = rootOffset + change.position + quickMenuController.updateHover(latestRootPosition, actions) + if (change.changedToUpIgnoreConsumed()) { + released = true + } + change.consume() + } + } + } + + quickMenuController.updateHover(latestRootPosition, actions) + val hoveredKey = quickMenuController.hoveredKey + quickMenuController.reset() + when (hoveredKey) { + EDIT_QUICK_ACTIONS_KEY -> onOpenEditor() + null -> onDismissQuickActions() + else -> { + val action = actions.firstOrNull { it.id == hoveredKey } + if (action != null) { + onQuickActionSelected(action.target) + } else { + onDismissQuickActions() + } + } + } + } + } +} + +@Composable +private fun NavigationQuickActionEditor( + show: Boolean, + selectedIds: List, + allActions: List, + onDismissRequest: () -> Unit, + onSave: (List) -> Unit, +) { + val allActionLookup = remember(allActions) { allActions.associateBy { it.id } } + val selectedState = remember(show, selectedIds, allActions) { + selectedIds + .mapNotNull { allActionLookup[it] } + .distinctBy { it.id } + .take(MaxNavigationQuickActions) + .map { it.id } + .ifEmpty { DefaultNavigationQuickActionIds } + .toMutableStateList() + } + val selectedLookup = selectedState.toSet() + val availableActions = allActions + .filterNot { it.id in selectedLookup } + .sortedBy { it.label } + val maxHint = stringResource(R.string.navigation_quick_max_hint, MaxNavigationQuickActions) + val bottomPadding = WindowInsets.safeDrawing.asPaddingValues().calculateBottomPadding() + + OverlayBottomSheet( + show = show, + title = stringResource(R.string.navigation_quick_edit_title), + onDismissRequest = onDismissRequest, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 560.dp) + .verticalScroll(rememberScrollState()) + .padding(bottom = bottomPadding + 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = maxHint, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + fontSize = 13.sp, + modifier = Modifier.padding(horizontal = 4.dp), + ) + NavigationQuickEditorSectionTitle(stringResource(R.string.navigation_quick_active_title)) + NavigationQuickEditorDropList( + activeIds = selectedState, + availableActions = availableActions, + allActionLookup = allActionLookup, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + TextButton( + text = stringResource(R.string.navigation_quick_cancel), + modifier = Modifier.weight(1f), + onClick = onDismissRequest, + ) + Button( + onClick = { onSave(selectedState.toList()) }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColorsPrimary(), + ) { + Text(text = stringResource(R.string.navigation_quick_save)) + } + } + } + } +} + +@Composable +private fun NavigationQuickEditorSectionTitle(text: String) { + Text( + text = text, + color = MiuixTheme.colorScheme.onSurface, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 4.dp), + ) +} + +@Composable +private fun NavigationQuickEditorDropList( + activeIds: MutableList, + availableActions: List, + allActionLookup: Map, +) { + val activeBounds = remember { mutableStateMapOf() } + val availableBounds = remember { mutableStateMapOf() } + var activeAreaBounds by remember { mutableStateOf(null) } + var availableAreaBounds by remember { mutableStateOf(null) } + var editorTopInRoot by remember { mutableFloatStateOf(0f) } + var dragState by remember { mutableStateOf(null) } + + fun previewActiveIds(): List { + val state = dragState ?: return activeIds.toList() + if (state.targetArea != NavigationEditorDropArea.Active) { + return activeIds.toList() + } + if (state.sourceArea == NavigationEditorDropArea.Available && activeIds.size >= MaxNavigationQuickActions) { + return activeIds.toList() + } + val preview = activeIds.filterNot { it == state.id }.toMutableList() + preview.add((state.insertIndex ?: preview.size).coerceIn(0, preview.size), state.id) + return preview + } + + fun resetDrag() { + dragState = null + } + + fun updateDrag(pointerRootY: Float) { + val state = dragState ?: return + val targetArea = when { + activeAreaBounds?.let { pointerRootY in it.top..it.bottom } == true -> NavigationEditorDropArea.Active + availableAreaBounds?.let { pointerRootY in it.top..it.bottom } == true -> NavigationEditorDropArea.Available + else -> state.targetArea + } + val insertIndex = if (targetArea == NavigationEditorDropArea.Active) { + findNavigationEditorInsertIndex( + ids = activeIds, + bounds = activeBounds, + draggedCenter = pointerRootY, + excludedId = if (state.sourceArea == NavigationEditorDropArea.Active) state.id else null, + ) + } else { + null + } + dragState = state.copy( + currentPointerRootY = pointerRootY, + targetArea = targetArea, + insertIndex = insertIndex, + ) + } + + fun finishDrag() { + val state = dragState ?: return + when (state.targetArea) { + NavigationEditorDropArea.Active -> { + if (state.sourceArea == NavigationEditorDropArea.Active) { + val fromIndex = activeIds.indexOf(state.id) + if (fromIndex >= 0) { + val item = activeIds.removeAt(fromIndex) + activeIds.add( + (state.insertIndex ?: activeIds.size).coerceIn( + 0, + activeIds.size + ), item + ) + } + } else if (activeIds.size < MaxNavigationQuickActions && state.id !in activeIds) { + activeIds.add( + (state.insertIndex ?: activeIds.size).coerceIn(0, activeIds.size), + state.id + ) + } + } + + NavigationEditorDropArea.Available -> { + if (state.sourceArea == NavigationEditorDropArea.Active) { + activeIds.remove(state.id) + } + } + } + resetDrag() + } + + val isFull = activeIds.size >= MaxNavigationQuickActions + val dragId = dragState?.id + val previewActiveIds = previewActiveIds() + val draggingToFullActive = dragState?.let { + it.sourceArea == NavigationEditorDropArea.Available && + it.targetArea == NavigationEditorDropArea.Active && + isFull + } == true + + Box( + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + editorTopInRoot = coordinates.positionInRoot().y + }, + ) { + Column( + modifier = Modifier.animateContentSize( + animationSpec = tween( + 180, + easing = FastOutSlowInEasing + ) + ), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + val position = coordinates.positionInRoot() + activeAreaBounds = Rect( + position.x, + position.y, + position.x + coordinates.size.width, + position.y + coordinates.size.height, + ) + } + .then( + if (draggingToFullActive) { + Modifier.border( + width = 1.dp, + color = MiuixTheme.colorScheme.dividerLine.copy(alpha = 0.9f), + shape = RoundedCornerShape(18.dp), + ) + } else { + Modifier + } + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (previewActiveIds.isEmpty()) { + Text( + text = stringResource(R.string.navigation_quick_active_empty), + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + fontSize = 13.sp, + modifier = Modifier.padding(8.dp), + ) + } + previewActiveIds.forEach { id -> + val action = allActionLookup[id] ?: return@forEach + NavigationQuickEditorActionRow( + action = action, + isDragged = false, + isPlaceholder = dragId == id, + modifier = Modifier + .onGloballyPositioned { coordinates -> + val position = coordinates.positionInRoot() + activeBounds[id] = NavigationEditorItemBounds( + top = position.y, + height = coordinates.size.height.toFloat(), + ) + }, + onDragStart = { pointerRootY -> + val bounds = activeBounds[id] + dragState = NavigationEditorDragState( + id = id, + sourceArea = NavigationEditorDropArea.Active, + targetArea = NavigationEditorDropArea.Active, + pointerOffsetInItemY = pointerRootY - (bounds?.top ?: pointerRootY), + currentPointerRootY = pointerRootY, + itemHeight = bounds?.height ?: 46f, + insertIndex = previewActiveIds.indexOf(id).coerceAtLeast(0), + ) + }, + onDrag = { updateDrag(it) }, + onDragEnd = ::finishDrag, + onDragCancel = ::resetDrag, + ) + } + } + + NavigationQuickEditorSectionTitle(stringResource(R.string.navigation_quick_available_title)) + Column( + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + val position = coordinates.positionInRoot() + availableAreaBounds = Rect( + position.x, + position.y, + position.x + coordinates.size.width, + position.y + coordinates.size.height, + ) + }, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (availableActions.isEmpty()) { + Text( + text = stringResource(R.string.navigation_quick_available_empty), + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + fontSize = 13.sp, + modifier = Modifier.padding(8.dp), + ) + } + availableActions.forEach { action -> + NavigationQuickEditorActionRow( + action = action, + isDragged = false, + isPlaceholder = dragId == action.id, + modifier = Modifier.onGloballyPositioned { coordinates -> + val position = coordinates.positionInRoot() + availableBounds[action.id] = NavigationEditorItemBounds( + top = position.y, + height = coordinates.size.height.toFloat(), + ) + }, + onDragStart = { pointerRootY -> + val bounds = availableBounds[action.id] + dragState = NavigationEditorDragState( + id = action.id, + sourceArea = NavigationEditorDropArea.Available, + targetArea = NavigationEditorDropArea.Available, + pointerOffsetInItemY = pointerRootY - (bounds?.top ?: pointerRootY), + currentPointerRootY = pointerRootY, + itemHeight = bounds?.height ?: 46f, + ) + }, + onDrag = { updateDrag(it) }, + onDragEnd = ::finishDrag, + onDragCancel = ::resetDrag, + ) + } + } + } + + dragState?.let { state -> + val action = + allActionLookup[state.id] ?: availableActions.firstOrNull { it.id == state.id } + if (action != null) { + NavigationQuickEditorActionRow( + action = action, + isDragged = true, + isPlaceholder = false, + dragEnabled = false, + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { + translationY = + state.currentPointerRootY - state.pointerOffsetInItemY - editorTopInRoot + } + .zIndex(8f), + onDragStart = {}, + onDrag = {}, + onDragEnd = {}, + onDragCancel = {}, + ) + } + } + } +} + +@Composable +private fun NavigationQuickEditorActionRow( + action: NavigationQuickAction, + isDragged: Boolean, + modifier: Modifier = Modifier, + isPlaceholder: Boolean = false, + dragEnabled: Boolean = true, + onDragStart: (Float) -> Unit, + onDrag: (Float) -> Unit, + onDragEnd: () -> Unit, + onDragCancel: () -> Unit, +) { + val scale by animateFloatAsState( + targetValue = if (isDragged) 1.03f else 1f, + animationSpec = spring(dampingRatio = 0.75f, stiffness = 360f), + label = "EditorRowScale", + ) + val alpha by animateFloatAsState( + targetValue = if (isPlaceholder) 0.2f else 1f, + animationSpec = tween(180, easing = FastOutSlowInEasing), + label = "EditorRowAlpha", + ) + val contentColor = MiuixTheme.colorScheme.onSurfaceContainer + Row( + modifier = modifier + .fillMaxWidth() + .animateContentSize(animationSpec = tween(180, easing = FastOutSlowInEasing)) + .height(46.dp) + .graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + shape = ContinuousCapsule + clip = false + } + .background(MiuixTheme.colorScheme.surfaceContainer, ContinuousCapsule) + .border( + 0.75.dp, + MiuixTheme.colorScheme.dividerLine.copy(alpha = 0.72f), + ContinuousCapsule + ) + .padding(start = 14.dp, end = 8.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = action.icon, + contentDescription = null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + AutoShrinkText( + text = action.label, + color = contentColor, + maxFontSize = 13.sp, + minFontSize = 9.sp, + modifier = Modifier.weight(1f), + ) + NavigationQuickDragHandle( + enabled = dragEnabled, + onDragStart = onDragStart, + onDrag = onDrag, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + ) + } +} + +@Composable +private fun NavigationQuickDragHandle( + enabled: Boolean, + onDragStart: (Float) -> Unit, + onDrag: (Float) -> Unit, + onDragEnd: () -> Unit, + onDragCancel: () -> Unit, +) { + var handleTopInRoot by remember { mutableFloatStateOf(0f) } + Box( + modifier = Modifier + .size(width = 36.dp, height = 40.dp) + .onGloballyPositioned { coordinates -> + handleTopInRoot = coordinates.positionInRoot().y + } + .then( + if (enabled) { + Modifier.pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + val downRootY = handleTopInRoot + down.position.y + onDragStart(downRootY) + onDrag(downRootY) + var cancelled = false + var dragging = true + while (dragging) { + val event = awaitPointerEvent() + event.changes.forEach { change -> + if (change.changedToUpIgnoreConsumed()) { + dragging = false + } else if (change.pressed) { + onDrag(handleTopInRoot + change.position.y) + change.consume() + } else { + cancelled = true + dragging = false + } + } + } + if (cancelled) onDragCancel() else onDragEnd() + } + } + } else { + Modifier + } + ), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Rounded.Menu, + contentDescription = null, + tint = MiuixTheme.colorScheme.onSurfaceContainer.copy(alpha = 0.62f), + modifier = Modifier.size(22.dp), + ) + } +} + +private enum class NavigationEditorDropArea { + Active, + Available, +} + +private data class NavigationEditorDragState( + val id: String, + val sourceArea: NavigationEditorDropArea, + val targetArea: NavigationEditorDropArea, + val pointerOffsetInItemY: Float, + val currentPointerRootY: Float, + val itemHeight: Float, + val insertIndex: Int? = null, +) + +private data class NavigationEditorItemBounds( + val top: Float, + val height: Float, +) + +private fun findNavigationEditorInsertIndex( + ids: List, + bounds: Map, + draggedCenter: Float, + excludedId: String? = null, +): Int { + val filteredIds = ids.filterNot { it == excludedId } + val boundaries = filteredIds.mapNotNull { id -> + bounds[id]?.let { it.top + it.height / 2f } + } + return boundaries.count { draggedCenter > it }.coerceIn(0, filteredIds.size) +} + +private class NavigationQuickActionsPopupPositionProvider( + private val spacingPx: Int, + private val windowPaddingPx: Int, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + val preferredX = anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2 + val minX = windowPaddingPx + val maxX = (windowSize.width - popupContentSize.width - windowPaddingPx) + .coerceAtLeast(minX) + val x = preferredX.coerceIn(minX, maxX) + + val preferredY = anchorBounds.top - popupContentSize.height - spacingPx + val maxY = (windowSize.height - popupContentSize.height - windowPaddingPx) + .coerceAtLeast(windowPaddingPx) + val fallbackY = anchorBounds.bottom + spacingPx + val y = if (preferredY >= windowPaddingPx) { + preferredY + } else { + fallbackY.coerceIn(windowPaddingPx, maxY) + } + + return IntOffset(x, y) + } +} diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt b/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt index 1929dda..9f5f738 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/Config.kt @@ -197,6 +197,7 @@ sealed class ConfigType { CARD, BUSINESS_EXTRA, BOUNDS, + LYRICS, } data class Manager(val managerType: ManagerType) : ConfigType() { diff --git a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt index 950bb28..e86179a 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/config/ModuleConfig.kt @@ -87,6 +87,7 @@ object ConfigKeys { const val MORE_DEBUG = "enable_more_debug_logging" const val MODULE_FAVORITE_CONFIG_NODES = "module_favorite_config_nodes" + const val MODULE_NAVIGATION_QUICK_ACTIONS = "module_navigation_quick_actions" } enum class ModuleNavigationBarMode( diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt index 878c04c..e94a312 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/ConfigScreen.kt @@ -138,6 +138,8 @@ private fun ConfigRoute.isOverlayRoute(): Boolean { @Composable fun ConfigScreen( bottomInnerPadding: Dp = 0.dp, + quickManagerTarget: ConfigType.ManagerType? = null, + onQuickManagerTargetHandled: () -> Unit = {}, onAppListModeChange: (Boolean) -> Unit = {}, onThemeModeChange: (Int) -> Unit = {}, onNavigationBarModeChange: (Int) -> Unit = {}, @@ -145,7 +147,33 @@ fun ConfigScreen( val context = LocalContext.current val prefsManager = remember { context.getPrefsManager() } - var routeStack by remember { mutableStateOf(listOf(ConfigRoute.Root)) } + fun findLyricsCategoryRoute(): ConfigRoute? { + return findConfigCategoryByTitleRes(REAREyeConfig, R.string.subcategory_lyrics)?.let( + ConfigRoute::Category + ) + } + + fun managerRoute(managerType: ConfigType.ManagerType?): ConfigRoute? { + return when (managerType) { + ConfigType.ManagerType.REAR_WALLPAPER -> ConfigRoute.RearWallpaperManager + ConfigType.ManagerType.BUSINESS -> ConfigRoute.BusinessManager + ConfigType.ManagerType.SCENE_ROUTE -> ConfigRoute.SceneRouteManager + ConfigType.ManagerType.CARD -> ConfigRoute.CardManager + ConfigType.ManagerType.BUSINESS_EXTRA -> ConfigRoute.BusinessExtraManager + ConfigType.ManagerType.BOUNDS -> ConfigRoute.CustomBoundsCompatManager + ConfigType.ManagerType.LYRICS -> findLyricsCategoryRoute() + null -> null + } + } + + var routeStack by remember { + mutableStateOf( + listOf( + ConfigRoute.Root, + managerRoute(quickManagerTarget) + ).filterNotNull() + ) + } val currentRoute = routeStack.last() val isOverlayMode = currentRoute.isOverlayRoute() val animatedRoute = remember(currentRoute, routeStack.size) { @@ -255,15 +283,33 @@ fun ConfigScreen( fun closeOverlayRoute() { routeScope.launch { - val newStack = routeStack.dropLast(1) - routeStack = newStack + routeStack = listOf(ConfigRoute.Root) delay(OVERLAY_ROUTE_EXIT_DURATION_MS) - if (!newStack.last().isOverlayRoute()) { - onAppListModeChange(false) - } + onAppListModeChange(false) } } + fun openManagerRoute(managerType: ConfigType.ManagerType?) { + val route = managerRoute(managerType) + if (route != null) { + openOverlayRoute(route) + } + } + + fun openManagerItem(item: ConfigItem) { + openManagerRoute((item.type as? ConfigType.Manager)?.managerType) + } + + LaunchedEffect(quickManagerTarget) { + val managerType = quickManagerTarget ?: return@LaunchedEffect + val route = managerRoute(managerType) ?: return@LaunchedEffect + routeStack = listOf(ConfigRoute.Root, route) + if (route.isOverlayRoute()) { + onAppListModeChange(true) + } + onQuickManagerTargetHandled() + } + Scaffold( topBar = { if (!isOverlayMode) { @@ -337,35 +383,7 @@ fun ConfigScreen( onOpenAppList = { item -> openOverlayRoute(ConfigRoute.AppList(item)) }, - onOpenManager = { item -> - when ((item.type as? ConfigType.Manager)?.managerType) { - ConfigType.ManagerType.REAR_WALLPAPER -> { - openOverlayRoute(ConfigRoute.RearWallpaperManager) - } - - ConfigType.ManagerType.BUSINESS -> { - openOverlayRoute(ConfigRoute.BusinessManager) - } - - ConfigType.ManagerType.SCENE_ROUTE -> { - openOverlayRoute(ConfigRoute.SceneRouteManager) - } - - ConfigType.ManagerType.CARD -> { - openOverlayRoute(ConfigRoute.CardManager) - } - - ConfigType.ManagerType.BUSINESS_EXTRA -> { - openOverlayRoute(ConfigRoute.BusinessExtraManager) - } - - ConfigType.ManagerType.BOUNDS -> { - openOverlayRoute(ConfigRoute.CustomBoundsCompatManager) - } - - null -> Unit - } - }, + onOpenManager = { item -> openManagerItem(item) }, onPreferenceChanged = handlePreferenceChanged, showFavoriteCategoryEntry = true, favoriteNodeCount = favoriteNodes.size, @@ -396,35 +414,7 @@ fun ConfigScreen( onOpenAppList = { item -> openOverlayRoute(ConfigRoute.AppList(item)) }, - onOpenManager = { item -> - when ((item.type as? ConfigType.Manager)?.managerType) { - ConfigType.ManagerType.REAR_WALLPAPER -> { - openOverlayRoute(ConfigRoute.RearWallpaperManager) - } - - ConfigType.ManagerType.BUSINESS -> { - openOverlayRoute(ConfigRoute.BusinessManager) - } - - ConfigType.ManagerType.SCENE_ROUTE -> { - openOverlayRoute(ConfigRoute.SceneRouteManager) - } - - ConfigType.ManagerType.CARD -> { - openOverlayRoute(ConfigRoute.CardManager) - } - - ConfigType.ManagerType.BUSINESS_EXTRA -> { - openOverlayRoute(ConfigRoute.BusinessExtraManager) - } - - ConfigType.ManagerType.BOUNDS -> { - openOverlayRoute(ConfigRoute.CustomBoundsCompatManager) - } - - null -> Unit - } - }, + onOpenManager = { item -> openManagerItem(item) }, onPreferenceChanged = handlePreferenceChanged, favoriteNodeIds = favoriteNodeIds, resolveFavoriteNodeId = { node -> @@ -450,35 +440,7 @@ fun ConfigScreen( onOpenAppList = { item -> openOverlayRoute(ConfigRoute.AppList(item)) }, - onOpenManager = { item -> - when ((item.type as? ConfigType.Manager)?.managerType) { - ConfigType.ManagerType.REAR_WALLPAPER -> { - openOverlayRoute(ConfigRoute.RearWallpaperManager) - } - - ConfigType.ManagerType.BUSINESS -> { - openOverlayRoute(ConfigRoute.BusinessManager) - } - - ConfigType.ManagerType.SCENE_ROUTE -> { - openOverlayRoute(ConfigRoute.SceneRouteManager) - } - - ConfigType.ManagerType.CARD -> { - openOverlayRoute(ConfigRoute.CardManager) - } - - ConfigType.ManagerType.BUSINESS_EXTRA -> { - openOverlayRoute(ConfigRoute.BusinessExtraManager) - } - - ConfigType.ManagerType.BOUNDS -> { - openOverlayRoute(ConfigRoute.CustomBoundsCompatManager) - } - - null -> Unit - } - }, + onOpenManager = { item -> openManagerItem(item) }, onPreferenceChanged = handlePreferenceChanged, emptyStateRes = R.string.config_favorites_empty, favoriteNodeIds = favoriteNodeIds, @@ -531,6 +493,21 @@ fun ConfigScreen( } } +private fun findConfigCategoryByTitleRes( + nodes: List, + titleRes: Int, +): ConfigCategory? { + nodes.forEach { node -> + if (node is ConfigCategory) { + if (node.titleRes == titleRes) return node + findConfigCategoryByTitleRes(node.children, titleRes)?.let { return it } + } else if (node is ConfigGroup) { + findConfigCategoryByTitleRes(node.children, titleRes)?.let { return it } + } + } + return null +} + @Composable private fun ConfigNodeList( nodes: List, diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5226772..2ca435a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -8,6 +8,18 @@ 仓库 配置 关于 + 组件管理 + 卡片管理 + 歌词管理 + 编辑 + 编辑快捷入口 + 最多显示 %1$d 个快捷入口 + 正在显示 + 可用快捷入口 + 暂无快捷入口 + 没有更多可用快捷入口 + 保存 + 取消 配置 GitHub 仓库 查看源码、问题反馈与发布版本 @@ -187,13 +199,13 @@ 可编辑资源 第三方资源 支持 AON - 组件模板管理器 + 组件管理 管理 组件 到模板文件的映射 - 通知路由管理器 + 通知路由 根据规则把应用通知映射到组件 - 组件额外设置管理器 + 组件额外设置 手动添加需要额外显示选项的组件 - 卡片管理器 + 卡片管理 管理常驻卡片、显示顺序与启用状态 允许焦点通知在背屏显示 必须先存在该焦点通知对应的组件或模板,否则系统仍会拒绝显示 @@ -500,11 +512,11 @@ 半透明 引用 查看我们引用了哪些库 - 背屏自定义界限管理 + 自定义界限 按应用管理背屏显示界限、DPI 与旋转行为 选择应用 选择需要使用背屏自定义界限的应用 - 背屏自定义界限管理 + 自定义界限 为每个应用单独调整背屏显示界限。仅作用于背屏显示。 暂无已配置应用 编辑应用界限 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 959f2d5..094b27f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,18 @@ Store Config About + Widget Management + Card Management + Lyrics Management + Edit + Edit Quick Actions + Show up to %1$d quick actions + Shown + Available Quick Actions + No quick actions selected + No more quick actions available + Save + Cancel Configuration GitHub Repository Source code, issues and releases @@ -187,13 +199,13 @@ Editable resource Third-party resource Support AON - Widget Template Manager + Widget Management Manage widget-to-template mapping only, without package route injection - Notification Route Manager + Notification Route Map application rule notifications to widgets - Widget Extra Settings Manager + Widget Extra Settings Manually add widgets that need extra display options - Card Manager + Card Management Manage resident cards, display order, and enabled state Allow focus notices on rear screen Allow notifications that already carry valid focus/rear params to pass the rear-screen whitelist. The matching widget business must already exist, otherwise the system will still reject it. @@ -500,11 +512,11 @@ Widget: %2$s · Priority: %3$d Semi-transparent Quote See which libraries we have referenced - Rear Screen Custom Bounds + Custom Bounds Manage each app\'s rear-screen bounds, DPI and rotation behavior Select Apps Choose apps that should use custom rear-screen bounds - Rear Screen Bounds Management + Custom Bounds Configure rear-screen display bounds per app. It only applies to the rear display. No app configured yet Edit App Bounds diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 567a463..9a23488 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "9.1.1" -kotlin = "2.3.20" +kotlin = "2.3.21" kotlinx-serialization-json = "1.11.0" ksp = "2.3.6" rovo89-xposed-api = "82" @@ -12,8 +12,8 @@ okhttp-logging = "5.3.2" junit = "4.13.2" androidx-test-junit = "1.3.0" androidx-test-espresso-core = "3.7.0" -compose-bom = "2026.03.01" -compose-foundation-style = "1.11.0-rc01" +compose-bom = "2026.05.00" +compose-foundation-style = "1.11.1" androidx-activity-compose = "1.13.0" androidx-lifecycle-runtime-compose = "2.10.0" miuix = "0.9.0" @@ -22,10 +22,11 @@ backdrop = "1.0.6" capsule = "2.1.3" lyricon = "0.1.70" superlyric = "3.3" -gson = "2.13.2" -dexkit = "2.1.0" -foundation-layout = "1.10.6" +gson = "2.14.0" +dexkit = "2.2.0" +foundation-layout = "1.11.1" mmkv = "2.4.0" +compose-icons = "2.2.1" [plugins] serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } @@ -72,4 +73,10 @@ gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } dexkit = { group = "org.luckypray", name = "dexkit", version.ref = "dexkit" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundation-layout" } mmkv = { group = "com.tencent", name = "mmkv-static", version.ref = "mmkv" } +compose-icons-material-symbols-outlined-cmp = { group = "com.composables", name = "icons-material-symbols-outlined-cmp", version.ref = "compose-icons" } +compose-icons-material-symbols-rounded-cmp = { group = "com.composables", name = "icons-material-symbols-rounded-cmp", version.ref = "compose-icons" } +compose-icons-material-symbols-sharp-cmp = { group = "com.composables", name = "icons-material-symbols-sharp-cmp", version.ref = "compose-icons" } +compose-icons-material-symbols-outlined-filled-cmp = { group = "com.composables", name = "icons-material-symbols-outlined-filled-cmp", version.ref = "compose-icons" } +compose-icons-material-symbols-rounded-filled-cmp = { group = "com.composables", name = "icons-material-symbols-rounded-filled-cmp", version.ref = "compose-icons" } +compose-icons-material-symbols-sharp-filled-cmp = { group = "com.composables", name = "icons-material-symbols-sharp-filled-cmp", version.ref = "compose-icons" } diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..7945d8f --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,48 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "17" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm-android:2025.3 From 415fbc82104a7bfa3cf44954efb012adcd4eb164 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 11 May 2026 14:33:59 +0800 Subject: [PATCH 24/27] feat: custom bounds profile export / import chore: clean code --- .../bounds/CustomBoundsCompatConfigCodec.kt | 153 +++++++------ .../config/CustomBoundsCompatManagerScreen.kt | 209 +++++++++++++++++- .../hk/uwu/reareye/ui/screen/AboutScreen.kt | 2 +- .../utils/other/AboutLibrariesTools.kt | 3 +- app/src/main/res/values-zh-rCN/strings.xml | 10 + app/src/main/res/values/strings.xml | 10 + 6 files changed, 313 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt b/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt index e58ff7a..f128b84 100644 --- a/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt +++ b/app/src/main/java/hk/uwu/reareye/repository/bounds/CustomBoundsCompatConfigCodec.kt @@ -1,5 +1,6 @@ package hk.uwu.reareye.repository.bounds +import android.util.Base64 import org.json.JSONArray import org.json.JSONObject @@ -67,6 +68,75 @@ object CustomBoundsCompatConfigCodec { 270, ) + private fun parseObject(obj: JSONObject): CustomBoundsCompatAppConfig? { + val packageName = obj.optString("packageName").trim() + if (packageName.isBlank()) return null + val aspectRatio = obj.optDouble("aspectRatio", 0.0) + .toFloat() + .takeIf { it > 0f } + ?: 0f + val parsedMode = CustomBoundsMode.fromString( + if (obj.has("mode")) obj.optString("mode") else null + ) + val mode = parsedMode ?: when { + obj.optInt("insetLeft", 0) != 0 || obj.optInt("insetTop", 0) != 0 || + obj.optInt("insetRight", 0) != 0 || obj.optInt("insetBottom", 0) != 0 -> { + CustomBoundsMode.EXACT_INSETS + } + + aspectRatio > 0f -> CustomBoundsMode.CUSTOM_RATIO + else -> CustomBoundsMode.AUTO_RATIO + } + val fillMode = CustomBoundsFillMode.fromString( + if (obj.has("fillMode")) obj.optString("fillMode") else null + ) ?: CustomBoundsFillMode.AUTO + return CustomBoundsCompatAppConfig( + packageName = packageName, + enabled = obj.optBoolean("enabled", true), + mode = mode, + aspectRatio = aspectRatio, + gravity = obj.optInt("gravity", DEFAULT_GRAVITY), + scale = obj.optDouble("scale", DEFAULT_SCALE.toDouble()) + .toFloat() + .takeIf { it > 0f } + ?: DEFAULT_SCALE, + densityDpi = obj.optInt("densityDpi", 0).coerceAtLeast(0), + rotationDegrees = normalizeRotation( + obj.optInt( + "rotationDegrees", + ROTATION_FOLLOW_SYSTEM + ) + ), + insetLeft = obj.optInt("insetLeft", 0).coerceAtLeast(0), + insetTop = obj.optInt("insetTop", 0).coerceAtLeast(0), + insetRight = obj.optInt("insetRight", 0).coerceAtLeast(0), + insetBottom = obj.optInt("insetBottom", 0).coerceAtLeast(0), + fillEnabled = obj.optBoolean("fillEnabled", false), + fillMode = fillMode, + fillColorArgb = obj.optInt("fillColorArgb", 0), + ) + } + + private fun toObject(item: CustomBoundsCompatAppConfig): JSONObject { + return JSONObject() + .put("version", 1) + .put("packageName", item.packageName.trim()) + .put("enabled", item.enabled) + .put("mode", item.mode.name.lowercase()) + .put("aspectRatio", item.aspectRatio.toDouble()) + .put("gravity", item.gravity) + .put("scale", item.scale.toDouble()) + .put("densityDpi", item.densityDpi) + .put("rotationDegrees", normalizeRotation(item.rotationDegrees)) + .put("insetLeft", item.insetLeft) + .put("insetTop", item.insetTop) + .put("insetRight", item.insetRight) + .put("insetBottom", item.insetBottom) + .put("fillEnabled", item.fillEnabled) + .put("fillMode", item.fillMode.name.lowercase()) + .put("fillColorArgb", item.fillColorArgb) + } + fun parse(raw: String?): List { if (raw.isNullOrBlank()) return emptyList() val arr = runCatching { JSONArray(raw) }.getOrNull() ?: return emptyList() @@ -74,52 +144,9 @@ object CustomBoundsCompatConfigCodec { val seenPackages = HashSet() for (i in 0 until arr.length()) { val obj = arr.optJSONObject(i) ?: continue - val packageName = obj.optString("packageName").trim() - if (packageName.isBlank() || !seenPackages.add(packageName)) continue - val aspectRatio = obj.optDouble("aspectRatio", 0.0) - .toFloat() - .takeIf { it > 0f } - ?: 0f - val parsedMode = CustomBoundsMode.fromString( - if (obj.has("mode")) obj.optString("mode") else null - ) - val mode = parsedMode ?: when { - obj.optInt("insetLeft", 0) != 0 || obj.optInt("insetTop", 0) != 0 || - obj.optInt("insetRight", 0) != 0 || obj.optInt("insetBottom", 0) != 0 -> { - CustomBoundsMode.EXACT_INSETS - } - - aspectRatio > 0f -> CustomBoundsMode.CUSTOM_RATIO - else -> CustomBoundsMode.AUTO_RATIO - } - val fillMode = CustomBoundsFillMode.fromString( - if (obj.has("fillMode")) obj.optString("fillMode") else null - ) ?: CustomBoundsFillMode.AUTO - out += CustomBoundsCompatAppConfig( - packageName = packageName, - enabled = obj.optBoolean("enabled", true), - mode = mode, - aspectRatio = aspectRatio, - gravity = obj.optInt("gravity", DEFAULT_GRAVITY), - scale = obj.optDouble("scale", DEFAULT_SCALE.toDouble()) - .toFloat() - .takeIf { it > 0f } - ?: DEFAULT_SCALE, - densityDpi = obj.optInt("densityDpi", 0).coerceAtLeast(0), - rotationDegrees = normalizeRotation( - obj.optInt( - "rotationDegrees", - ROTATION_FOLLOW_SYSTEM - ) - ), - insetLeft = obj.optInt("insetLeft", 0).coerceAtLeast(0), - insetTop = obj.optInt("insetTop", 0).coerceAtLeast(0), - insetRight = obj.optInt("insetRight", 0).coerceAtLeast(0), - insetBottom = obj.optInt("insetBottom", 0).coerceAtLeast(0), - fillEnabled = obj.optBoolean("fillEnabled", false), - fillMode = fillMode, - fillColorArgb = obj.optInt("fillColorArgb", 0), - ) + val config = parseObject(obj) ?: continue + if (!seenPackages.add(config.packageName)) continue + out += config } return out } @@ -131,28 +158,22 @@ object CustomBoundsCompatConfigCodec { .filter { it.packageName.isNotBlank() } .distinctBy { it.packageName } .sortedBy { it.packageName.lowercase() } - .forEach { item -> - arr.put( - JSONObject() - .put("packageName", item.packageName.trim()) - .put("enabled", item.enabled) - .put("mode", item.mode.name.lowercase()) - .put("aspectRatio", item.aspectRatio.toDouble()) - .put("gravity", item.gravity) - .put("scale", item.scale.toDouble()) - .put("densityDpi", item.densityDpi) - .put("rotationDegrees", normalizeRotation(item.rotationDegrees)) - .put("insetLeft", item.insetLeft) - .put("insetTop", item.insetTop) - .put("insetRight", item.insetRight) - .put("insetBottom", item.insetBottom) - .put("fillEnabled", item.fillEnabled) - .put("fillMode", item.fillMode.name.lowercase()) - .put("fillColorArgb", item.fillColorArgb) - ) - } + .forEach { item -> arr.put(toObject(item)) } }.toString() + fun encodeRuleFile(config: CustomBoundsCompatAppConfig): String { + val json = toObject(config).toString() + return Base64.encodeToString(json.toByteArray(Charsets.UTF_8), Base64.NO_WRAP) + } + + fun decodeRuleFile(raw: String?): CustomBoundsCompatAppConfig? { + if (raw.isNullOrBlank()) return null + return runCatching { + val decoded = Base64.decode(raw.trim(), Base64.DEFAULT).toString(Charsets.UTF_8) + parseObject(JSONObject(decoded)) + }.getOrNull() + } + fun normalizeForPackages( configs: List, packageNames: Set, diff --git a/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt index 22a2bed..e08754c 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/components/config/CustomBoundsCompatManagerScreen.kt @@ -3,6 +3,8 @@ package hk.uwu.reareye.ui.components.config import android.annotation.SuppressLint import android.widget.Toast import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -44,6 +46,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt +import com.composables.icons.materialsymbols.MaterialSymbols +import com.composables.icons.materialsymbols.rounded.Deployed_code_update +import com.composables.icons.materialsymbols.rounded.File_export import hk.uwu.reareye.R import hk.uwu.reareye.repository.bounds.CustomBoundsCompatAppConfig import hk.uwu.reareye.repository.bounds.CustomBoundsCompatConfigCodec @@ -68,6 +73,7 @@ import hk.uwu.reareye.ui.theme.rememberAcrylicHazeState import hk.uwu.reareye.ui.theme.rememberAcrylicHazeStyle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import top.yukonga.miuix.kmp.basic.Button import top.yukonga.miuix.kmp.basic.ButtonDefaults @@ -133,7 +139,7 @@ fun CustomBoundsCompatManagerScreen( val scrollBehavior = MiuixScrollBehavior() val hazeState = rememberAcrylicHazeState() val hazeStyle = rememberAcrylicHazeStyle() - rememberCoroutineScope() + val scope = rememberCoroutineScope() val configs = remember { mutableStateListOf() } var loaded by remember { mutableStateOf(false) } var dataCardsVisible by remember { mutableStateOf(false) } @@ -157,6 +163,10 @@ fun CustomBoundsCompatManagerScreen( var draftFillMode by remember { mutableIntStateOf(0) } var draftFillColor by remember { mutableStateOf("#FF000000") } var draftFillColorError by remember { mutableStateOf(false) } + var exportTarget by remember { mutableStateOf(null) } + var pendingOverwriteConfig by remember { mutableStateOf(null) } + var rejectedImportPackage by remember { mutableStateOf(null) } + var importErrorMessage by remember { mutableStateOf(null) } val appSelectorItem = remember { ConfigItem( @@ -193,6 +203,42 @@ fun CustomBoundsCompatManagerScreen( prefsManager.putString(ConfigKeys.CUSTOM_BOUNDS_COMPAT_CONFIG_DATA, encoded) } + fun toast(resId: Int) { + Toast.makeText(context, context.getString(resId), Toast.LENGTH_SHORT).show() + } + + fun applyImportedConfig(config: CustomBoundsCompatAppConfig) { + val nextConfigs = configs.toMutableList() + val index = nextConfigs.indexOfFirst { it.packageName == config.packageName } + if (index >= 0) { + nextConfigs[index] = config + } else { + nextConfigs += config + } + replaceConfigs(nextConfigs) + persist() + toast(R.string.custom_bounds_rule_import_success) + } + + fun packageInstalled(packageName: String): Boolean { + return runCatching { + context.packageManager.getPackageInfo(packageName, 0) + true + }.getOrDefault(false) + } + + fun handleImportedConfig(config: CustomBoundsCompatAppConfig) { + if (!packageInstalled(config.packageName)) { + rejectedImportPackage = config.packageName + return + } + if (configs.any { it.packageName == config.packageName }) { + pendingOverwriteConfig = config + } else { + applyImportedConfig(config) + } + } + fun syncAfterAppSelection() { val nextConfigs = loadConfigsFromPrefs() replaceConfigs(nextConfigs) @@ -301,6 +347,55 @@ fun CustomBoundsCompatManagerScreen( ).show() } + fun exportFileName(packageName: String): String { + val safePackage = packageName.replace(Regex("[^A-Za-z0-9._-]"), "_") + return "${safePackage}_${System.currentTimeMillis()}.rbt" + } + + val exportLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/octet-stream"), + ) { uri -> + val target = exportTarget + exportTarget = null + if (uri == null || target == null) return@rememberLauncherForActivityResult + scope.launch { + val success = withContext(Dispatchers.IO) { + runCatching { + val raw = CustomBoundsCompatConfigCodec.encodeRuleFile(target) + context.contentResolver.openOutputStream(uri)?.use { output -> + output.write(raw.toByteArray(Charsets.UTF_8)) + } ?: error("openOutputStream failed") + }.isSuccess + } + if (success) { + toast(R.string.custom_bounds_rule_export_success) + } else { + toast(R.string.custom_bounds_rule_export_failed) + } + } + } + + val importLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + ) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + scope.launch { + val importedConfig = withContext(Dispatchers.IO) { + runCatching { + val raw = context.contentResolver.openInputStream(uri)?.use { input -> + input.readBytes().toString(Charsets.UTF_8) + } ?: error("openInputStream failed") + CustomBoundsCompatConfigCodec.decodeRuleFile(raw) + }.getOrNull() + } + if (importedConfig == null) { + importErrorMessage = context.getString(R.string.custom_bounds_rule_import_invalid) + } else { + handleImportedConfig(importedConfig) + } + } + } + LaunchedEffect(Unit) { delay(220) replaceConfigs(withContext(Dispatchers.IO) { loadConfigsFromPrefs() }) @@ -343,6 +438,20 @@ fun CustomBoundsCompatManagerScreen( } }, actions = { + IconButton(onClick = { + if (loaded) importLauncher.launch( + arrayOf( + "application/octet-stream", + "text/plain", + "*/*" + ) + ) + }) { + Icon( + imageVector = MaterialSymbols.Rounded.Deployed_code_update, + contentDescription = null + ) + } IconButton(onClick = { if (loaded) showAppSelector = true }) { Icon(imageVector = Icons.Filled.Add, contentDescription = null) } @@ -428,10 +537,22 @@ fun CustomBoundsCompatManagerScreen( badges = customBoundsBadges(item), onCardClick = { openEditDialog(item) }, leftAction = { - ModuleStyleIconAction( - icon = Icons.Rounded.EditNote, - onClick = { openEditDialog(item) }, - ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + ModuleStyleIconAction( + icon = Icons.Rounded.EditNote, + onClick = { openEditDialog(item) }, + ) + ModuleStyleIconAction( + icon = MaterialSymbols.Rounded.File_export, + onClick = { + exportTarget = item + exportLauncher.launch(exportFileName(item.packageName)) + }, + ) + } }, rightAction = { ModuleStyleDeleteAction( @@ -612,6 +733,84 @@ fun CustomBoundsCompatManagerScreen( } } } + + OverlayDialog( + show = rejectedImportPackage != null, + title = stringResource(R.string.custom_bounds_rule_import_rejected_title), + onDismissRequest = { rejectedImportPackage = null }, + ) { + DialogFormColumn { + Text( + text = stringResource( + R.string.custom_bounds_rule_import_app_missing, + rejectedImportPackage.orEmpty(), + ), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + Button( + onClick = { rejectedImportPackage = null }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_confirm)) + } + } + } + + OverlayDialog( + show = pendingOverwriteConfig != null, + title = stringResource(R.string.custom_bounds_rule_import_overwrite_title), + onDismissRequest = { pendingOverwriteConfig = null }, + ) { + DialogFormColumn { + Text( + text = stringResource( + R.string.custom_bounds_rule_import_overwrite_message, + pendingOverwriteConfig?.packageName.orEmpty(), + ), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + Button( + onClick = { + pendingOverwriteConfig?.let(::applyImportedConfig) + pendingOverwriteConfig = null + }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.custom_bounds_rule_import_overwrite_confirm)) + } + Button( + onClick = { pendingOverwriteConfig = null }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_cancel)) + } + } + } + + OverlayDialog( + show = importErrorMessage != null, + title = stringResource(R.string.custom_bounds_rule_import_failed_title), + onDismissRequest = { importErrorMessage = null }, + ) { + DialogFormColumn { + Text( + text = importErrorMessage.orEmpty(), + style = MiuixTheme.textStyles.body2, + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + ) + Button( + onClick = { importErrorMessage = null }, + colors = ButtonDefaults.buttonColorsPrimary(), + modifier = Modifier.fillMaxWidth(), + ) { + Text(stringResource(R.string.rear_widget_confirm)) + } + } + } } private fun formatFloat(value: Float): String = diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 94b90b0..2eecbdf 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -1126,7 +1126,7 @@ private fun LicenseContent( overscrollEffect = null, ) { items(data.libraries) { - LibraryItem(it,data.licenses) + LibraryItem(it, data.licenses) } } diff --git a/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt index 46f6b8d..f8e5f69 100644 --- a/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt +++ b/app/src/main/java/hk/uwu/reareye/utils/other/AboutLibrariesTools.kt @@ -148,8 +148,7 @@ fun LibraryItem(lib: Library, licenses: Map) { if (lib.developers.isNotEmpty() || lib.licenses.isNotEmpty()) { HorizontalDivider( modifier = Modifier - .height(16.dp) - , + .height(16.dp), thickness = 1.dp, color = MiuixTheme.colorScheme.outline.copy(alpha = 0.5f) ) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2ca435a..b484ba6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -578,4 +578,14 @@ 已停用 未填充 填充 %1$s + 规则已导出 + 规则导出失败 + 规则已导入 + 规则文件无效或已损坏 + 导入失败 + 拒绝导入 + 规则目标应用 %1$s 不存在,已拒绝导入。 + 覆盖规则? + %1$s 已有自定义界限规则,是否覆盖? + 覆盖 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 094b27f..37af484 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -578,4 +578,14 @@ Widget: %2$s · Priority: %3$d Disabled Fill off Fill %1$s + Rule exported + Failed to export rule + Rule imported + The rule file is invalid or corrupted + Import failed + Import rejected + Target app %1$s does not exist. Import was rejected. + Overwrite rule? + %1$s already has a custom bounds rule. Overwrite it? + Overwrite From 8fbaadd85220e427c4f3d8a405e631f931fbe9a8 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 11 May 2026 20:21:18 +0800 Subject: [PATCH 25/27] feat: codename & show module version on about screen --- .../java/hk/uwu/reareye/ui/screen/AboutScreen.kt | 12 ++++++++++++ .../main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt | 8 ++++++++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + gradle.properties | 1 + 5 files changed, 23 insertions(+) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt index 2eecbdf..eb5aa1b 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/AboutScreen.kt @@ -963,6 +963,18 @@ private fun AboutDeviceInfoCard( insideMargin = deviceInfoRowPadding, ) + BasicComponent( + title = stringResource(R.string.module_version_label), + summary = rememberVersionText(), + insideMargin = deviceInfoRowPadding, + ) + + BasicComponent( + title = stringResource(R.string.version_codename_label), + summary = AppProperties.PROJECT_APP_VERSION_CODENAME, + insideMargin = deviceInfoRowPadding, + ) + if (subscreenVer != "UNKNOWN") { BasicComponent( title = stringResource(R.string.subsceen_version), diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt index 51fc7f7..8bb1125 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/HomeScreen.kt @@ -275,6 +275,7 @@ fun HomeScreen(bottomInnerPadding: Dp = 0.dp) { EasterEggType.HUAWEI_ULTIMATE_DESIGN -> "ULTIMATE DESIGN" else -> AppProperties.BUILD_CHANNEL } + val versionCodename = AppProperties.PROJECT_APP_VERSION_CODENAME Scaffold( topBar = { @@ -458,6 +459,7 @@ fun HomeScreen(bottomInnerPadding: Dp = 0.dp) { ModuleInfoCard( activated = isActivated, moduleVersion = moduleVersion, + versionCodename = versionCodename, releaseChannel = releaseChannel, easterEggType = easterEggType ) @@ -818,6 +820,7 @@ private fun UpdateWarningCard(currentHash: String, latestHash: String, useMonetC private fun ModuleInfoCard( activated: Boolean, moduleVersion: String, + versionCodename: String, releaseChannel: String, easterEggType: EasterEggType ) { @@ -854,6 +857,11 @@ private fun ModuleInfoCard( value = moduleVersion, ) Spacer(modifier = Modifier.height(12.dp)) + InfoLine( + title = androidx.compose.ui.res.stringResource(R.string.version_codename_label), + value = versionCodename, + ) + Spacer(modifier = Modifier.height(12.dp)) InfoLine( title = androidx.compose.ui.res.stringResource(R.string.home_status_channel), value = releaseChannel, diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b484ba6..b4c83a6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -424,6 +424,7 @@ Root 不可用 部分功能依赖 Root 权限, 请在Root管理器中授予本应用Root权限 模块版本 + 版本代号 快捷终止背屏中心 快捷终止主题管理 已终止 %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37af484..2f838b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -424,6 +424,7 @@ Widget: %2$s · Priority: %3$d Root unavailable Some features require root privileges. Please grant this application root privileges using your root manager Module Version + Version Codename Force stop Subscreen Center Force stop Theme Manager Stopped %1$s diff --git a/gradle.properties b/gradle.properties index c1b82ac..6accec8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,3 +17,4 @@ project.android.buildToolsVersion=37.0.0 project.android.compileSdkMinor=0 project.app.packageName=hk.uwu.reareye project.app.versionName="1.0.7" +project.app.versionCodename="CircuitBreaker" From 1f37e9b6eefcaa89410c0e4f49111c9e5161eb93 Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 11 May 2026 20:53:39 +0800 Subject: [PATCH 26/27] chore: remove qodana ci --- .github/workflows/qodana_code_quality.yml | 39 ----------------------- qodana.yaml | 2 +- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 .github/workflows/qodana_code_quality.yml diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml deleted file mode 100644 index 406c121..0000000 --- a/.github/workflows/qodana_code_quality.yml +++ /dev/null @@ -1,39 +0,0 @@ -#-------------------------------------------------------------------------------# -# Discover additional configuration options in our documentation # -# https://www.jetbrains.com/help/qodana/github.html # -#-------------------------------------------------------------------------------# - -name: Qodana -on: - workflow_dispatch: - pull_request: - push: - branches: - - dev - -jobs: - qodana: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - checks: write - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2025.3 - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} - with: - # When pr-mode is set to true, Qodana analyzes only the files that have been changed - pr-mode: false - use-caches: true - post-pr-comment: true - use-annotations: true - # Upload Qodana results (SARIF, other artifacts, logs) as an artifact to the job - upload-result: true - # quick-fixes available in Ultimate and Ultimate Plus plans - push-fixes: 'none' \ No newline at end of file diff --git a/qodana.yaml b/qodana.yaml index 7945d8f..cb2204e 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -23,7 +23,7 @@ profile: # paths: # - -projectJDK: "17" #(Applied in CI/CD pipeline) +projectJDK: "21" #(Applied in CI/CD pipeline) #Execute shell command before Qodana execution (Applied in CI/CD pipeline) #bootstrap: sh ./prepare-qodana.sh From b8baeb08a7c271d024c5e4cc274ad1d5b085c93a Mon Sep 17 00:00:00 2001 From: killerprojecte Date: Mon, 11 May 2026 22:56:47 +0800 Subject: [PATCH 27/27] chore: directly query content schema uri on post-install & upgrade Gradle to 9.5.0 --- .../uwu/reareye/ui/screen/RearStoreScreen.kt | 28 +++++++++++++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt index 2760bab..fe78e55 100644 --- a/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt +++ b/app/src/main/java/hk/uwu/reareye/ui/screen/RearStoreScreen.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.graphics.BitmapFactory +import android.util.Log import android.view.View import android.view.ViewGroup import android.webkit.WebChromeClient @@ -620,12 +621,27 @@ private fun launchPostInstallUri( val scheme = uri.scheme?.lowercase(Locale.ROOT).orEmpty() if (scheme == "content") { runCatching { - context.startActivity( - Intent(Intent.ACTION_VIEW, uri).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - ) + context.applicationContext.contentResolver.query(uri, null, null, null, null) + ?.use { cursor -> + val columns = (0 until cursor.columnCount).joinToString(", ") { index -> + cursor.getColumnName(index) + } + val rowCount = if (cursor.moveToFirst()) 1 else 0 + val debugValues = if (rowCount > 0) { + (0 until cursor.columnCount).joinToString(", ") { index -> + val value = runCatching { cursor.getString(index) }.getOrNull() + "${cursor.getColumnName(index)}=${value ?: ""}" + } + } else { + "" + } + Log.d( + "RearStorePostInstall", + "query content uri=$uri columns=[$columns] rows=$rowCount values=[$debugValues]", + ) + } ?: Log.d("RearStorePostInstall", "query content uri=$uri result=") + }.onFailure { error -> + Log.e("RearStorePostInstall", "query content uri=$uri failed", error) } return } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 035ae8c..5b0fe58 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Mar 09 17:02:30 CST 2026 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME \ No newline at end of file