From d971f79fe367e82d842ce910591df18094439c94 Mon Sep 17 00:00:00 2001 From: HmnDev-Tech <256042304+HmnDev-Tech@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:35:52 +0500 Subject: [PATCH] feat(ui): redesign settings like Shevery, add Shevery plaque, motion blur and system theme color - Add dedicated Shevery privileges card highlighting Shevery support over Shizuku - Clean up settings design: remove cluttered elements, pill bars, and duplicate subtitles - Group settings into clean Material 3 Expressive sections with section headers matching Shevery - Add Motion Blur toggle in Appearance with directional AGSL scroll blur modifier - Add 'Use system theme color' setting in Appearance - Update ShizukuManager and PermissionTools for independent Shevery/Shizuku actions - Add localized strings across all 6 language catalogs (en, ru, kk, es, zh, pt) --- .../com/hmndev/chronika/data/AppSettings.kt | 1 + .../com/hmndev/chronika/data/SettingsStore.kt | 2 + .../com/hmndev/chronika/i18n/StringsEn.kt | 9 + .../com/hmndev/chronika/i18n/StringsEs.kt | 9 + .../com/hmndev/chronika/i18n/StringsKk.kt | 9 + .../com/hmndev/chronika/i18n/StringsPt.kt | 9 + .../com/hmndev/chronika/i18n/StringsRu.kt | 9 + .../com/hmndev/chronika/i18n/StringsZh.kt | 9 + .../hmndev/chronika/shizuku/ShizukuManager.kt | 107 ++++++++---- .../com/hmndev/chronika/ui/ChronikaRoot.kt | 4 +- .../chronika/ui/components/MotionBlur.kt | 154 +++++++++++++++++ .../ui/components/SettingComponents.kt | 50 +++--- .../chronika/ui/screens/JournalScreen.kt | 5 + .../chronika/ui/screens/SettingsScreen.kt | 158 +++++++++++++++--- .../hmndev/chronika/util/PermissionTools.kt | 13 +- 15 files changed, 451 insertions(+), 97 deletions(-) create mode 100644 app/src/main/java/com/hmndev/chronika/ui/components/MotionBlur.kt diff --git a/app/src/main/java/com/hmndev/chronika/data/AppSettings.kt b/app/src/main/java/com/hmndev/chronika/data/AppSettings.kt index 668b88f..92adc53 100644 --- a/app/src/main/java/com/hmndev/chronika/data/AppSettings.kt +++ b/app/src/main/java/com/hmndev/chronika/data/AppSettings.kt @@ -25,6 +25,7 @@ data class AppSettings( val requiredKeywords: String = "", val themeMode: String = "system", val dynamicColor: Boolean = true, + val motionBlur: Boolean = false, val compact: Boolean = false, val showRawPayload: Boolean = false, val language: String = "", diff --git a/app/src/main/java/com/hmndev/chronika/data/SettingsStore.kt b/app/src/main/java/com/hmndev/chronika/data/SettingsStore.kt index a7d65f2..409af01 100644 --- a/app/src/main/java/com/hmndev/chronika/data/SettingsStore.kt +++ b/app/src/main/java/com/hmndev/chronika/data/SettingsStore.kt @@ -76,6 +76,7 @@ object SettingsStore { putString("requiredKeywords", next.requiredKeywords) putString("themeMode", next.themeMode) putBoolean("dynamicColor", next.dynamicColor) + putBoolean("motionBlur", next.motionBlur) putBoolean("compact", next.compact) putBoolean("showRawPayload", next.showRawPayload) putString("language", next.language) @@ -112,6 +113,7 @@ object SettingsStore { requiredKeywords = prefs.getString("requiredKeywords", defaults.requiredKeywords).orEmpty(), themeMode = prefs.getString("themeMode", defaults.themeMode).orEmpty().ifBlank { defaults.themeMode }, dynamicColor = prefs.getBoolean("dynamicColor", defaults.dynamicColor), + motionBlur = prefs.getBoolean("motionBlur", defaults.motionBlur), compact = prefs.getBoolean("compact", defaults.compact), showRawPayload = prefs.getBoolean("showRawPayload", defaults.showRawPayload), language = prefs.getString("language", defaults.language).orEmpty(), diff --git a/app/src/main/java/com/hmndev/chronika/i18n/StringsEn.kt b/app/src/main/java/com/hmndev/chronika/i18n/StringsEn.kt index fb17799..27b9c07 100644 --- a/app/src/main/java/com/hmndev/chronika/i18n/StringsEn.kt +++ b/app/src/main/java/com/hmndev/chronika/i18n/StringsEn.kt @@ -107,6 +107,11 @@ internal val EN_STRINGS: Map = mapOf( "settings.import_active_desc" to "Read the current shade via dumpsys notification", "settings.open_shizuku" to "Open Shizuku", "settings.open_shizuku_desc" to "Install, start or check the service", + "settings.shevery" to "Shevery", + "settings.shevery_desc" to "Instead of Shizuku, you can grant privileges via Shevery for fast startup and direct background operations", + "settings.shevery_allow" to "Allow Shevery", + "settings.open_shevery" to "Open Shevery", + "settings.open_shevery_desc" to "Install, start or check Shevery manager", "settings.capture" to "Capture", "settings.capture_sub" to "What and how to save", "settings.capture_enabled" to "Capture enabled", @@ -162,6 +167,10 @@ internal val EN_STRINGS: Map = mapOf( "settings.theme_dark" to "Dark", "settings.dynamic" to "Dynamic color", "settings.dynamic_desc" to "Wallpaper palette on Android 12+", + "settings.motion_blur" to "Motion blur", + "settings.motion_blur_desc" to "Blur content dynamically while scrolling lists and transitions", + "settings.system_theme_color" to "Use system theme color", + "settings.system_theme_color_desc" to "Pull accent colors from system palette and wallpaper (Android 12+)", "settings.compact" to "Compact density", "settings.compact_desc" to "More entries on screen", "settings.test" to "Check notification", diff --git a/app/src/main/java/com/hmndev/chronika/i18n/StringsEs.kt b/app/src/main/java/com/hmndev/chronika/i18n/StringsEs.kt index 1c289ca..635b71f 100644 --- a/app/src/main/java/com/hmndev/chronika/i18n/StringsEs.kt +++ b/app/src/main/java/com/hmndev/chronika/i18n/StringsEs.kt @@ -107,6 +107,11 @@ internal val ES_STRINGS: Map = mapOf( "settings.import_active_desc" to "Leer el panel actual con dumpsys notification", "settings.open_shizuku" to "Abrir Shizuku", "settings.open_shizuku_desc" to "Instalar, iniciar o comprobar el servicio", + "settings.shevery" to "Shevery", + "settings.shevery_desc" to "En lugar de Shizuku, puedes otorgar privilegios a través de Shevery para un inicio rápido y operaciones directas en segundo plano", + "settings.shevery_allow" to "Permitir Shevery", + "settings.open_shevery" to "Abrir Shevery", + "settings.open_shevery_desc" to "Instalar, iniciar o verificar el gestor Shevery", "settings.capture" to "Captura", "settings.capture_sub" to "Qué y cómo guardar", "settings.capture_enabled" to "Captura activada", @@ -162,6 +167,10 @@ internal val ES_STRINGS: Map = mapOf( "settings.theme_dark" to "Oscuro", "settings.dynamic" to "Color dinámico", "settings.dynamic_desc" to "Paleta del fondo de pantalla en Android 12+", + "settings.motion_blur" to "Desenfoque de movimiento", + "settings.motion_blur_desc" to "Desenfocar el contenido dinámicamente al desplazarse por listas y transiciones", + "settings.system_theme_color" to "Usar colores del sistema", + "settings.system_theme_color_desc" to "Obtener colores de acento de la paleta del sistema y fondo de pantalla (Android 12+)", "settings.compact" to "Densidad compacta", "settings.compact_desc" to "Más registros en pantalla", "settings.test" to "Comprobar notificación", diff --git a/app/src/main/java/com/hmndev/chronika/i18n/StringsKk.kt b/app/src/main/java/com/hmndev/chronika/i18n/StringsKk.kt index 51153d1..52f0647 100644 --- a/app/src/main/java/com/hmndev/chronika/i18n/StringsKk.kt +++ b/app/src/main/java/com/hmndev/chronika/i18n/StringsKk.kt @@ -107,6 +107,11 @@ internal val KK_STRINGS: Map = mapOf( "settings.import_active_desc" to "Ағымдағы панельді dumpsys notification арқылы оқу", "settings.open_shizuku" to "Shizuku ашу", "settings.open_shizuku_desc" to "Қызметті орнату, іске қосу немесе тексеру", + "settings.shevery" to "Shevery", + "settings.shevery_desc" to "Shizuku орнына жылдам іске қосу және тікелей фондық жұмыс үшін Shevery арқылы артықшылықтар бере аласыз", + "settings.shevery_allow" to "Shevery рұқсаты", + "settings.open_shevery" to "Shevery-ді ашу", + "settings.open_shevery_desc" to "Shevery менеджерін орнату, іске қосу немесе тексеру", "settings.capture" to "Ұстау", "settings.capture_sub" to "Нені және қалай сақтау", "settings.capture_enabled" to "Ұстау қосулы", @@ -162,6 +167,10 @@ internal val KK_STRINGS: Map = mapOf( "settings.theme_dark" to "Қараңғы", "settings.dynamic" to "Динамикалық түс", "settings.dynamic_desc" to "Android 12+ жүйесіндегі тұсқағаз палитрасы", + "settings.motion_blur" to "Қозғалыстағы бұлдырату", + "settings.motion_blur_desc" to "Тізімдерді айналдыру және ауысулар кезінде мазмұнды динамикалық түрде бұлдырату", + "settings.system_theme_color" to "Жүйелік тақырып түсін пайдалану", + "settings.system_theme_color_desc" to "Жүйелік палитра мен тұсқағаздан екпінді түстерді алу (Android 12+)", "settings.compact" to "Ықшам тығыздық", "settings.compact_desc" to "Экранда көбірек жазба", "settings.test" to "Хабарламаны тексеру", diff --git a/app/src/main/java/com/hmndev/chronika/i18n/StringsPt.kt b/app/src/main/java/com/hmndev/chronika/i18n/StringsPt.kt index 03e47ac..1f5273a 100644 --- a/app/src/main/java/com/hmndev/chronika/i18n/StringsPt.kt +++ b/app/src/main/java/com/hmndev/chronika/i18n/StringsPt.kt @@ -107,6 +107,11 @@ internal val PT_STRINGS: Map = mapOf( "settings.import_active_desc" to "Ler o painel atual via dumpsys notification", "settings.open_shizuku" to "Abrir Shizuku", "settings.open_shizuku_desc" to "Instalar, iniciar ou verificar o serviço", + "settings.shevery" to "Shevery", + "settings.shevery_desc" to "Em vez do Shizuku, você pode conceder privilégios via Shevery para inicialização rápida e operações diretas em segundo plano", + "settings.shevery_allow" to "Permitir Shevery", + "settings.open_shevery" to "Abrir Shevery", + "settings.open_shevery_desc" to "Instalar, iniciar ou verificar o gerenciador Shevery", "settings.capture" to "Captura", "settings.capture_sub" to "O que e como salvar", "settings.capture_enabled" to "Captura ativada", @@ -162,6 +167,10 @@ internal val PT_STRINGS: Map = mapOf( "settings.theme_dark" to "Escuro", "settings.dynamic" to "Cor dinâmica", "settings.dynamic_desc" to "Paleta do papel de parede no Android 12+", + "settings.motion_blur" to "Desfoque de movimento", + "settings.motion_blur_desc" to "Desfocar o conteúdo dinamicamente ao rolar listas e transições", + "settings.system_theme_color" to "Usar cor do tema do sistema", + "settings.system_theme_color_desc" to "Obter cores de destaque da paleta do sistema e papel de parede (Android 12+)", "settings.compact" to "Densidade compacta", "settings.compact_desc" to "Mais registros na tela", "settings.test" to "Verificar notificação", diff --git a/app/src/main/java/com/hmndev/chronika/i18n/StringsRu.kt b/app/src/main/java/com/hmndev/chronika/i18n/StringsRu.kt index b9e079a..2f6ff86 100644 --- a/app/src/main/java/com/hmndev/chronika/i18n/StringsRu.kt +++ b/app/src/main/java/com/hmndev/chronika/i18n/StringsRu.kt @@ -107,6 +107,11 @@ internal val RU_STRINGS: Map = mapOf( "settings.import_active_desc" to "Прочитать текущую панель через dumpsys notification", "settings.open_shizuku" to "Открыть Shizuku", "settings.open_shizuku_desc" to "Установить, запустить или проверить сервис", + "settings.shevery" to "Shevery", + "settings.shevery_desc" to "Вместо Shizuku можно предоставить привилегии через Shevery для быстрого запуска и прямой фоновой работы", + "settings.shevery_allow" to "Разрешить Shevery", + "settings.open_shevery" to "Открыть Shevery", + "settings.open_shevery_desc" to "Установить, запустить или проверить менеджер Shevery", "settings.capture" to "Захват", "settings.capture_sub" to "Что и как сохранять", "settings.capture_enabled" to "Захват включён", @@ -162,6 +167,10 @@ internal val RU_STRINGS: Map = mapOf( "settings.theme_dark" to "Тёмная", "settings.dynamic" to "Динамический цвет", "settings.dynamic_desc" to "Палитра обоев на Android 12+", + "settings.motion_blur" to "Размытие в движении", + "settings.motion_blur_desc" to "Динамически размывать контент при прокрутке списков и переходах", + "settings.system_theme_color" to "Палитра цветов из системы", + "settings.system_theme_color_desc" to "Использовать системные акцентные цвета и палитру обоев (Android 12+)", "settings.compact" to "Компактная плотность", "settings.compact_desc" to "Больше записей на экране", "settings.test" to "Проверить уведомление", diff --git a/app/src/main/java/com/hmndev/chronika/i18n/StringsZh.kt b/app/src/main/java/com/hmndev/chronika/i18n/StringsZh.kt index f49af63..42e3e3f 100644 --- a/app/src/main/java/com/hmndev/chronika/i18n/StringsZh.kt +++ b/app/src/main/java/com/hmndev/chronika/i18n/StringsZh.kt @@ -107,6 +107,11 @@ internal val ZH_STRINGS: Map = mapOf( "settings.import_active_desc" to "通过 dumpsys notification 读取当前通知栏", "settings.open_shizuku" to "打开 Shizuku", "settings.open_shizuku_desc" to "安装、启动或检查服务", + "settings.shevery" to "Shevery", + "settings.shevery_desc" to "你可以通过 Shevery 授予特权来代替 Shizuku,实现极速启动与直接后台操作", + "settings.shevery_allow" to "允许 Shevery", + "settings.open_shevery" to "打开 Shevery", + "settings.open_shevery_desc" to "安装、启动或检查 Shevery 管理器", "settings.capture" to "捕获", "settings.capture_sub" to "保存什么以及如何保存", "settings.capture_enabled" to "捕获已开启", @@ -162,6 +167,10 @@ internal val ZH_STRINGS: Map = mapOf( "settings.theme_dark" to "深色", "settings.dynamic" to "动态取色", "settings.dynamic_desc" to "Android 12+ 的壁纸配色", + "settings.motion_blur" to "动态模糊", + "settings.motion_blur_desc" to "在滚动列表和页面过渡时对内容应用动态模糊", + "settings.system_theme_color" to "使用系统主题颜色", + "settings.system_theme_color_desc" to "从系统调色板和壁纸中提取强调色(Android 12+)", "settings.compact" to "紧凑密度", "settings.compact_desc" to "屏幕显示更多记录", "settings.test" to "检查通知", diff --git a/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt b/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt index 9c02985..8eceeb4 100644 --- a/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt +++ b/app/src/main/java/com/hmndev/chronika/shizuku/ShizukuManager.kt @@ -31,6 +31,12 @@ data class ShizukuState( val version: Int = 0, val message: String = S["shizuku.not_running"], val backend: PrivilegedBackend = PrivilegedBackend.NONE, + val sheveryAvailable: Boolean = false, + val sheveryPermissionGranted: Boolean = false, + val sheveryVersion: Int = 0, + val shizukuAvailable: Boolean = false, + val shizukuPermissionGranted: Boolean = false, + val shizukuVersion: Int = 0, ) data class ShellResult( @@ -96,49 +102,86 @@ object ShizukuManager { fun refresh(forcedPermission: Boolean? = null) { val sheveryAvailable = runCatching { SheveryPrivilege.pingBinder() }.getOrDefault(false) - if (sheveryAvailable) { - val version = runCatching { SheveryPrivilege.getVersion() }.getOrDefault(0) - val permission = when { - forcedPermission != null -> forcedPermission - else -> runCatching { - SheveryPrivilege.checkSelfPermission() == PackageManager.PERMISSION_GRANTED - }.getOrDefault(false) - } - val message = when { - !permission -> S["shizuku.permission_needed"] - else -> S.f("shizuku.active", version).replace("Shizuku", "Shevery") - } - _state.value = ShizukuState( - available = true, - permissionGranted = permission, - version = version, - message = message, - backend = PrivilegedBackend.SHEVERY - ) - return + val sheveryVersion = if (sheveryAvailable) runCatching { SheveryPrivilege.getVersion() }.getOrDefault(0) else 0 + val sheveryPermission = when { + !sheveryAvailable -> false + forcedPermission != null && _state.value.backend == PrivilegedBackend.SHEVERY -> forcedPermission + else -> runCatching { + SheveryPrivilege.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + }.getOrDefault(false) } val shizukuAvailable = runCatching { Shizuku.pingBinder() }.getOrDefault(false) - val version = if (shizukuAvailable) { + val shizukuVersion = if (shizukuAvailable) { runCatching { Shizuku.getVersion() }.getOrDefault(0) } else { 0 } - val permission = when { + val shizukuPermission = when { !shizukuAvailable -> false - forcedPermission != null -> forcedPermission + forcedPermission != null && _state.value.backend == PrivilegedBackend.SHIZUKU -> forcedPermission runCatching { Shizuku.isPreV11() }.getOrDefault(true) -> false else -> runCatching { Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED }.getOrDefault(false) } + + if (sheveryAvailable) { + val message = when { + !sheveryPermission -> S["shizuku.permission_needed"] + else -> S.f("shizuku.active", sheveryVersion).replace("Shizuku", "Shevery") + } + _state.value = ShizukuState( + available = true, + permissionGranted = sheveryPermission, + version = sheveryVersion, + message = message, + backend = PrivilegedBackend.SHEVERY, + sheveryAvailable = true, + sheveryPermissionGranted = sheveryPermission, + sheveryVersion = sheveryVersion, + shizukuAvailable = shizukuAvailable, + shizukuPermissionGranted = shizukuPermission, + shizukuVersion = shizukuVersion + ) + return + } + val backend = if (shizukuAvailable) PrivilegedBackend.SHIZUKU else PrivilegedBackend.NONE val message = when { !shizukuAvailable -> S["shizuku.unavailable"] - !permission -> S["shizuku.permission_needed"] - else -> S.f("shizuku.active", version) + !shizukuPermission -> S["shizuku.permission_needed"] + else -> S.f("shizuku.active", shizukuVersion) + } + _state.value = ShizukuState( + available = shizukuAvailable, + permissionGranted = shizukuPermission, + version = shizukuVersion, + message = message, + backend = backend, + sheveryAvailable = false, + sheveryPermissionGranted = false, + sheveryVersion = 0, + shizukuAvailable = shizukuAvailable, + shizukuPermissionGranted = shizukuPermission, + shizukuVersion = shizukuVersion + ) + } + + fun requestSheveryPermission() { + runCatching { + SheveryPrivilege.requestPermission(REQUEST_CODE) + }.onFailure { + _state.value = _state.value.copy(message = S.f("shizuku.request_failed", it.message)) + } + } + + fun requestShizukuPermission() { + runCatching { + Shizuku.requestPermission(REQUEST_CODE) + }.onFailure { + _state.value = _state.value.copy(message = S.f("shizuku.request_failed", it.message)) } - _state.value = ShizukuState(shizukuAvailable, permission, version, message, backend) } fun requestPermission() { @@ -148,18 +191,10 @@ object ShizukuManager { return } if (current.backend == PrivilegedBackend.SHEVERY || SheveryPrivilege.pingBinder()) { - runCatching { - SheveryPrivilege.requestPermission(REQUEST_CODE) - }.onFailure { - _state.value = current.copy(message = S.f("shizuku.request_failed", it.message)) - } + requestSheveryPermission() return } - runCatching { - Shizuku.requestPermission(REQUEST_CODE) - }.onFailure { - _state.value = current.copy(message = S.f("shizuku.request_failed", it.message)) - } + requestShizukuPermission() } suspend fun runCommand(command: String): ShellResult = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/hmndev/chronika/ui/ChronikaRoot.kt b/app/src/main/java/com/hmndev/chronika/ui/ChronikaRoot.kt index a50b455..4269249 100644 --- a/app/src/main/java/com/hmndev/chronika/ui/ChronikaRoot.kt +++ b/app/src/main/java/com/hmndev/chronika/ui/ChronikaRoot.kt @@ -292,7 +292,9 @@ fun ChronikaRoot(viewModel: JournalViewModel) { PermissionTools.openAccessibilitySettings(context) }, onOpenShizuku = { PermissionTools.openShizuku(context) }, - onRequestShizuku = { ShizukuManager.requestPermission() }, + onRequestShizuku = { ShizukuManager.requestShizukuPermission() }, + onOpenShevery = { PermissionTools.openShevery(context) }, + onRequestShevery = { ShizukuManager.requestSheveryPermission() }, onGrantListener = { scope.launch { notify(viewModel.grantListener()) diff --git a/app/src/main/java/com/hmndev/chronika/ui/components/MotionBlur.kt b/app/src/main/java/com/hmndev/chronika/ui/components/MotionBlur.kt new file mode 100644 index 0000000..21b7ea7 --- /dev/null +++ b/app/src/main/java/com/hmndev/chronika/ui/components/MotionBlur.kt @@ -0,0 +1,154 @@ +package com.hmndev.chronika.ui.components + +import android.graphics.RenderEffect +import android.graphics.RuntimeShader +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.withFrameNanos +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.blur +import androidx.compose.ui.graphics.asComposeRenderEffect +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.isActive +import org.intellij.lang.annotations.Language +import kotlin.math.abs +import kotlin.math.min + +@Language("AGSL") +private const val DIRECTIONAL_BLUR_AGSL = """ + uniform shader composable; + uniform float2 resolution; + uniform float scrollVelocity; + uniform float isHorizontal; + uniform float blurScale; + + half4 main(float2 fragCoord) { + const int SAMPLES = 10; + half4 color = half4(0.0); + float totalWeight = 0.0; + + float blurMagnitude = clamp(scrollVelocity * 22.0 * blurScale, -60.0, 60.0); + + for (int i = 0; i < SAMPLES; i++) { + float offset = (float(i) / float(SAMPLES - 1) - 0.5) * blurMagnitude; + + float2 sampleCoord; + if (isHorizontal > 0.5) { + float clampedX = clamp(fragCoord.x + offset, 0.0, resolution.x); + sampleCoord = float2(clampedX, fragCoord.y); + } else { + float clampedY = clamp(fragCoord.y + offset, 0.0, resolution.y); + sampleCoord = float2(fragCoord.x, clampedY); + } + + float weight = 1.0 - abs(offset / (abs(blurMagnitude) + 0.001)) * 0.5; + + color += composable.eval(sampleCoord) * weight; + totalWeight += weight; + } + + return color / totalWeight; + } +""" + +@Composable +private fun rememberLazyVelocity(listState: LazyListState): Animatable { + val animatedVelocity = remember { Animatable(0f) } + LaunchedEffect(listState) { + var prevIndex = listState.firstVisibleItemIndex + var prevOffset = listState.firstVisibleItemScrollOffset + var prevTimeNanos = 0L + while (isActive) { + var next = animatedVelocity.value + withFrameNanos { frameTimeNanos -> + val currentIndex = listState.firstVisibleItemIndex + val currentOffset = listState.firstVisibleItemScrollOffset + if (prevTimeNanos != 0L) { + val dtMs = (frameTimeNanos - prevTimeNanos) / 1_000_000.0f + if (dtMs in 1f..100f) { + val totalDelta = + ((currentIndex - prevIndex) * 80f) + (currentOffset - prevOffset) + val target = (totalDelta / dtMs).coerceIn(-3f, 3f) + next = if (abs(totalDelta) > 0.1f && listState.isScrollInProgress) { + animatedVelocity.value * 0.35f + target * 0.65f + } else { + val decayed = animatedVelocity.value * 0.45f + if (abs(decayed) < 0.01f) 0f else decayed + } + } else { + next = 0f + } + } + prevIndex = currentIndex + prevOffset = currentOffset + prevTimeNanos = frameTimeNanos + } + animatedVelocity.snapTo(next) + } + } + LaunchedEffect(listState.isScrollInProgress) { + if (!listState.isScrollInProgress) { + animatedVelocity.animateTo(0f, animationSpec = tween(60)) + } + } + return animatedVelocity +} + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +@Composable +private fun directionalBlurLayer( + velocity: Animatable, + blurScale: Float +): Modifier { + if (blurScale <= 0.001f) return Modifier + val shader = remember { RuntimeShader(DIRECTIONAL_BLUR_AGSL) } + return Modifier.graphicsLayer { + val vel = velocity.value + if (abs(vel) > 0.05f) { + shader.setFloatUniform("resolution", size.width, size.height) + shader.setFloatUniform("scrollVelocity", vel) + shader.setFloatUniform("isHorizontal", 0.0f) + shader.setFloatUniform("blurScale", blurScale) + renderEffect = RenderEffect + .createRuntimeShaderEffect(shader, "composable") + .asComposeRenderEffect() + } else { + renderEffect = null + } + } +} + +private fun Modifier.velocityFallbackBlur( + velocity: Animatable +): Modifier = composed { + val vel = velocity.value + val maxRadius = 12f + val radius = (min(abs(vel) / 3f, 1f) * maxRadius).dp + if (radius > 0.01.dp) this.blur(radius) else this +} + +/** + * Directional motion blur driven by a [LazyListState]. + */ +fun Modifier.scrollMotionBlur( + listState: LazyListState, + enabled: Boolean +): Modifier = composed { + if (!enabled) return@composed this + val velocity = rememberLazyVelocity(listState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + this.then(directionalBlurLayer(velocity, 1.0f)) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + this.velocityFallbackBlur(velocity) + } else { + this + } +} diff --git a/app/src/main/java/com/hmndev/chronika/ui/components/SettingComponents.kt b/app/src/main/java/com/hmndev/chronika/ui/components/SettingComponents.kt index addfadd..e1ce43d 100644 --- a/app/src/main/java/com/hmndev/chronika/ui/components/SettingComponents.kt +++ b/app/src/main/java/com/hmndev/chronika/ui/components/SettingComponents.kt @@ -33,46 +33,34 @@ import androidx.compose.ui.unit.dp @Composable fun SettingsSection( title: String, - subtitle: String, - accent: Color, + subtitle: String = "", + accent: Color = MaterialTheme.colorScheme.primary, modifier: Modifier = Modifier, content: @Composable () -> Unit ) { - Column(modifier = modifier.fillMaxWidth()) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - Modifier - .width(26.dp) - .height(4.dp) - .clip(RoundedCornerShape(50)) - .background(accent) + Column( + modifier = modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) { + if (title.isNotBlank()) { + Text( + text = title, + modifier = Modifier.padding(start = 12.dp, bottom = 8.dp), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.SemiBold ) - Spacer(Modifier.width(9.dp)) - Column { - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } } - Spacer(Modifier.height(10.dp)) Surface( - shape = RoundedCornerShape(30.dp), - color = MaterialTheme.colorScheme.surfaceContainerLow, - tonalElevation = 0.dp, - shadowElevation = 1.dp + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh ) { Column(Modifier.padding(vertical = 4.dp)) { content() } } - Spacer(Modifier.height(20.dp)) } } @@ -239,7 +227,7 @@ fun SettingsGroupDivider() { Modifier .padding(horizontal = 16.dp) .fillMaxWidth() - .height(1.dp) - .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f)) + .height(0.5.dp) + .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f)) ) } diff --git a/app/src/main/java/com/hmndev/chronika/ui/screens/JournalScreen.kt b/app/src/main/java/com/hmndev/chronika/ui/screens/JournalScreen.kt index d705065..51a03fb 100644 --- a/app/src/main/java/com/hmndev/chronika/ui/screens/JournalScreen.kt +++ b/app/src/main/java/com/hmndev/chronika/ui/screens/JournalScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import com.hmndev.chronika.ui.components.scrollMotionBlur import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape @@ -129,8 +131,11 @@ fun JournalScreen( val groups = remember(filtered) { filtered.groupBy { TimeTools.dayKey(it.postedAt) } } + val listState = rememberLazyListState() LazyColumn( + state = listState, + modifier = Modifier.scrollMotionBlur(listState, enabled = settings.motionBlur), contentPadding = PaddingValues(bottom = device.dp(126.dp)), verticalArrangement = Arrangement.spacedBy(0.dp) ) { diff --git a/app/src/main/java/com/hmndev/chronika/ui/screens/SettingsScreen.kt b/app/src/main/java/com/hmndev/chronika/ui/screens/SettingsScreen.kt index c95d7a9..86d3a1a 100644 --- a/app/src/main/java/com/hmndev/chronika/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/hmndev/chronika/ui/screens/SettingsScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material.icons.rounded.Apps import androidx.compose.material.icons.rounded.Archive import androidx.compose.material.icons.rounded.AutoAwesome import androidx.compose.material.icons.rounded.Bolt +import androidx.compose.material.icons.rounded.BlurOn import androidx.compose.material.icons.rounded.CleaningServices import androidx.compose.material.icons.rounded.CloudDownload import androidx.compose.material.icons.rounded.Code @@ -38,6 +39,9 @@ import androidx.compose.material.icons.rounded.Security import androidx.compose.material.icons.rounded.Storage import androidx.compose.material.icons.rounded.Terminal import androidx.compose.material.icons.rounded.Tune +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.ui.text.font.FontWeight +import com.hmndev.chronika.ui.components.scrollMotionBlur import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.FilledTonalButton @@ -91,6 +95,8 @@ fun SettingsScreen( onOpenAccessibilitySettings: () -> Unit, onOpenShizuku: () -> Unit, onRequestShizuku: () -> Unit, + onOpenShevery: () -> Unit = {}, + onRequestShevery: () -> Unit = {}, onGrantListener: () -> Unit, onGrantAccessibility: () -> Unit, onImportShizuku: () -> Unit, @@ -116,6 +122,7 @@ fun SettingsScreen( mutableStateOf(settings.uiScale) } val focusManager = LocalFocusManager.current + val listState = rememberLazyListState() fun commitMaxEntries() { val parsed = maxEntriesDraft.toIntOrNull()?.coerceIn(500, 200000) @@ -127,6 +134,8 @@ fun SettingsScreen( } LazyColumn( + state = listState, + modifier = Modifier.scrollMotionBlur(listState, enabled = settings.motionBlur), contentPadding = PaddingValues( start = device.screenPadding, end = device.screenPadding, @@ -136,31 +145,18 @@ fun SettingsScreen( verticalArrangement = Arrangement.spacedBy(0.dp) ) { item(key = "heading") { - Column { - Text( - text = S["settings.eyebrow"], - style = MaterialTheme.typography.labelSmall, - color = scheme.primary - ) - Text( - text = S["settings.title"], - style = MaterialTheme.typography.displaySmall, - color = scheme.onSurface - ) - Text( - text = S["settings.subtitle"], - style = MaterialTheme.typography.bodyMedium, - color = scheme.onSurfaceVariant - ) - Spacer(Modifier.height(16.dp)) - } + Text( + text = S["settings.title"], + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = scheme.onSurface, + modifier = Modifier.padding(start = 4.dp, top = 8.dp, bottom = 16.dp) + ) } item(key = "access") { SettingsSection( - title = S["settings.access"], - subtitle = S["settings.access_sub"], - accent = Coral + title = S["settings.access"] ) { AccessRow( title = S["settings.listener"], @@ -175,14 +171,114 @@ fun SettingsScreen( granted = access.accessibility, onClick = onOpenAccessibilitySettings ) + } + } + + item(key = "shevery_card") { + SettingsSection( + title = S["settings.shevery"] + ) { + Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + SettingIcon(Icons.Rounded.Security, enabled = true, tint = scheme.primary) + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f)) { + Text( + text = S["settings.shevery"], + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = scheme.onSurface + ) + Text( + text = S["settings.shevery_desc"], + style = MaterialTheme.typography.bodySmall, + color = scheme.onSurfaceVariant + ) + } + } + Spacer(Modifier.height(8.dp)) + Text( + text = if (shizuku.sheveryAvailable) { + if (shizuku.sheveryPermissionGranted) { + S.f("shizuku.active", shizuku.sheveryVersion).replace("Shizuku", "Shevery") + } else { + S["shizuku.permission_needed"] + } + } else { + S["shizuku.not_running"].replace("Shizuku", "Shevery") + }, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + color = if (shizuku.sheveryAvailable && shizuku.sheveryPermissionGranted) { + Lime + } else if (shizuku.sheveryAvailable) { + scheme.tertiary + } else { + scheme.outline + } + ) + } + + if (shizuku.sheveryAvailable && !shizuku.sheveryPermissionGranted) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button(onClick = onRequestShevery, shape = CircleShape) { + Text(S["settings.shevery_allow"]) + } + } + } + + if (shizuku.sheveryAvailable && shizuku.sheveryPermissionGranted) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilledTonalButton(onClick = onGrantListener, shape = CircleShape) { + Text(S["settings.grant_listener"]) + } + FilledTonalButton(onClick = onGrantAccessibility, shape = CircleShape) { + Text(S["settings.grant_access"]) + } + } + SettingAction( + title = S["settings.import_active"], + description = S["settings.import_active_desc"], + icon = Icons.Rounded.CloudDownload, + onClick = onImportShizuku + ) + } + SettingsGroupDivider() + SettingAction( + title = S["settings.open_shevery"], + description = S["settings.open_shevery_desc"], + icon = Icons.Rounded.Security, + onClick = onOpenShevery + ) + } + } + + item(key = "shizuku") { + SettingsSection( + title = "Shizuku" + ) { AccessRow( title = "Shizuku", - description = shizuku.message, - granted = shizuku.available && shizuku.permissionGranted, + description = if (shizuku.shizukuAvailable) { + if (shizuku.shizukuPermissionGranted) { + S.f("shizuku.active", shizuku.shizukuVersion) + } else { + S["shizuku.permission_needed"] + } + } else { + S["shizuku.not_running"] + }, + granted = shizuku.shizukuAvailable && shizuku.shizukuPermissionGranted, onClick = onOpenShizuku ) - if (shizuku.available && !shizuku.permissionGranted) { + if (shizuku.shizukuAvailable && !shizuku.shizukuPermissionGranted) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -192,7 +288,7 @@ fun SettingsScreen( } } } - if (shizuku.permissionGranted) { + if (shizuku.shizukuAvailable && shizuku.shizukuPermissionGranted && !shizuku.sheveryAvailable) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) @@ -497,13 +593,21 @@ fun SettingsScreen( } SettingsGroupDivider() SettingSwitch( - title = S["settings.dynamic"], - description = S["settings.dynamic_desc"], + title = S["settings.system_theme_color"], + description = S["settings.system_theme_color_desc"], checked = settings.dynamicColor, onCheckedChange = { value -> onUpdate { it.copy(dynamicColor = value) } }, icon = Icons.Rounded.ColorLens ) SettingsGroupDivider() + SettingSwitch( + title = S["settings.motion_blur"], + description = S["settings.motion_blur_desc"], + checked = settings.motionBlur, + onCheckedChange = { value -> onUpdate { it.copy(motionBlur = value) } }, + icon = Icons.Rounded.BlurOn + ) + SettingsGroupDivider() SettingSwitch( title = S["settings.compact"], description = S["settings.compact_desc"], diff --git a/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt b/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt index 7757fe0..4a99169 100644 --- a/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt +++ b/app/src/main/java/com/hmndev/chronika/util/PermissionTools.kt @@ -80,9 +80,8 @@ object PermissionTools { } } - fun openShizuku(context: Context) { + fun openShevery(context: Context) { val launch = context.packageManager.getLaunchIntentForPackage("com.hamondev.shevery") - ?: context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api") if (launch != null) { launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(launch) @@ -91,6 +90,16 @@ object PermissionTools { } } + fun openShizuku(context: Context) { + val launch = context.packageManager.getLaunchIntentForPackage("moe.shizuku.privileged.api") + if (launch != null) { + launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launch) + } else { + openUrl(context, "https://shizuku.rikka.app/") + } + } + fun openUrl(context: Context, url: String) { runCatching { context.startActivity(