diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1511544..55a7e45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -184,4 +184,7 @@ dependencies { implementation(libs.others.reorderable) implementation(libs.androidx.splashscreen) + + implementation(libs.coil.compose) + implementation(libs.coil.gif) } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/MainActivity.kt b/app/src/main/java/li/songe/gkd/MainActivity.kt index 9ae57f3..640b96c 100644 --- a/app/src/main/java/li/songe/gkd/MainActivity.kt +++ b/app/src/main/java/li/songe/gkd/MainActivity.kt @@ -5,7 +5,6 @@ import android.content.Context import androidx.activity.compose.setContent import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.rememberNavController import com.dylanc.activityresult.launcher.PickContentLauncher @@ -29,7 +28,6 @@ import li.songe.gkd.util.storeFlow @AndroidEntryPoint class MainActivity : CompositionActivity({ - installSplashScreen() useLifeCycleLog() val launcher = StartActivityLauncher(this) @@ -47,7 +45,8 @@ class MainActivity : CompositionActivity({ } setContent { - val navController = rememberNavController() + + val navController = rememberNavController() AppTheme { UpgradeDialog() CompositionLocalProvider( diff --git a/app/src/main/java/li/songe/gkd/composition/CompositionActivity.kt b/app/src/main/java/li/songe/gkd/composition/CompositionActivity.kt index 02234f2..ced7864 100644 --- a/app/src/main/java/li/songe/gkd/composition/CompositionActivity.kt +++ b/app/src/main/java/li/songe/gkd/composition/CompositionActivity.kt @@ -3,6 +3,8 @@ package li.songe.gkd.composition import android.content.res.Configuration import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.enableEdgeToEdge +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen open class CompositionActivity( private val block: CompositionActivity.(Bundle?) -> Unit, @@ -26,6 +28,8 @@ open class CompositionActivity( } override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + enableEdgeToEdge() super.onCreate(savedInstanceState) block(savedInstanceState) } diff --git a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt index 05701f9..81b00eb 100644 --- a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt +++ b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt @@ -82,6 +82,20 @@ data class SubscriptionRaw( } } } + + @IgnoredOnParcel + val allExampleUrls by lazy { + mutableListOf().apply { + if (exampleUrls != null) { + addAll(exampleUrls) + } + rules.forEach { r -> + if (r.exampleUrls != null) { + addAll(r.exampleUrls) + } + } + } + } } @Serializable diff --git a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt index 946ad8d..9d71c11 100644 --- a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt @@ -58,6 +58,7 @@ import kotlinx.serialization.encodeToString import li.songe.gkd.data.SubsConfig import li.songe.gkd.data.SubscriptionRaw import li.songe.gkd.db.DbSet +import li.songe.gkd.ui.destinations.GroupItemPageDestination import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.Singleton @@ -66,6 +67,7 @@ import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow +import li.songe.gkd.util.navigate @RootNavGraph @Destination(style = ProfileTransitions::class) @@ -238,16 +240,32 @@ fun AppItemPage( } }, confirmButton = { - TextButton(onClick = { - val groupAppText = Singleton.json.encodeToString( - appRaw?.copy( - groups = listOf(showGroupItemVal) + Row { + if (showGroupItemVal.allExampleUrls.isNotEmpty()) { + TextButton(onClick = { + setShowGroupItem(null) + navController.navigate( + GroupItemPageDestination( + subsInt = subsItemId, + appId = appId, + groupKey = showGroupItemVal.key + ) + ) + }) { + Text(text = "查看图片") + } + } + TextButton(onClick = { + val groupAppText = Singleton.json.encodeToString( + appRaw?.copy( + groups = listOf(showGroupItemVal) + ) ) - ) - ClipboardUtils.copyText(groupAppText) - ToastUtils.showShort("复制成功") - }) { - Text(text = "复制规则组") + ClipboardUtils.copyText(groupAppText) + ToastUtils.showShort("复制成功") + }) { + Text(text = "复制规则组") + } } }) } diff --git a/app/src/main/java/li/songe/gkd/ui/ControlPage.kt b/app/src/main/java/li/songe/gkd/ui/ControlPage.kt index 81f9b6f..0b9d83a 100644 --- a/app/src/main/java/li/songe/gkd/ui/ControlPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/ControlPage.kt @@ -15,11 +15,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -48,7 +45,6 @@ import li.songe.gkd.util.usePollState val controlNav = BottomNavItem(label = "主页", icon = SafeR.ic_home, route = "settings") -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ControlPage() { val context = LocalContext.current as MainActivity @@ -64,116 +60,103 @@ fun ControlPage() { } val canDrawOverlays by usePollState { Settings.canDrawOverlays(context) } - Scaffold( - topBar = { - TopAppBar(title = { - Text( - text = "GKD" - ) + + Column( + modifier = Modifier.verticalScroll( + state = rememberScrollState() + ) + ) { + if (!notifEnabled) { + AuthCard(title = "通知权限", desc = "用于启动后台服务,展示服务运行状态", onAuthClick = { + val intent = Intent() + intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid) + context.startActivity(intent) }) - }, - ) { padding -> + Divider() + } + + if (!gkdAccessRunning) { + AuthCard(title = "无障碍权限", + desc = "用于获取屏幕信息,点击屏幕上的控件", + onAuthClick = { + if (notifEnabled) { + appScope.launchTry(Dispatchers.IO) { + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + // android.content.ActivityNotFoundException + // https://bugly.qq.com/v2/crash-reporting/crashes/d0ce46b353/113010?pid=1 + context.startActivity(intent) + } + } else { + ToastUtils.showShort("必须先开启[通知权限]") + } + }) + Divider() + } + + if (!canDrawOverlays) { + AuthCard(title = "悬浮窗权限", + desc = "用于后台提示,显示保存快照按钮等功能", + onAuthClick = { + val intent = Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + ) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + }) + Divider() + } + + if (gkdAccessRunning) { + TextSwitch(name = "服务开启", + desc = "保持服务开启,根据订阅规则匹配屏幕目标节点", + checked = store.enableService, + onCheckedChange = { + updateStorage( + storeFlow, store.copy( + enableService = it + ) + ) + }) + Divider() + } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + navController.navigate(ClickLogPageDestination) + } + .padding(10.dp, 5.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "点击记录", fontSize = 18.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "如误触可在此快速定位关闭规则", fontSize = 14.sp + ) + } + Icon(imageVector = Icons.Default.KeyboardArrowRight, contentDescription = null) + } + Divider() + Column( modifier = Modifier - .verticalScroll( - state = rememberScrollState() - ) - .padding(padding) + .fillMaxWidth() + .padding(10.dp, 5.dp) ) { - if (!notifEnabled) { - AuthCard(title = "通知权限", - desc = "用于启动后台服务,展示服务运行状态", - onAuthClick = { - val intent = Intent() - intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS - intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid) - context.startActivity(intent) - }) - Divider() + Text(text = subsStatus, fontSize = 18.sp) + if (latestRecordDesc != null) { + Text( + text = "最近点击: $latestRecordDesc", fontSize = 14.sp + ) } - - if (!gkdAccessRunning) { - AuthCard(title = "无障碍权限", - desc = "用于获取屏幕信息,点击屏幕上的控件", - onAuthClick = { - if (notifEnabled) { - appScope.launchTry(Dispatchers.IO) { - val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - // android.content.ActivityNotFoundException - // https://bugly.qq.com/v2/crash-reporting/crashes/d0ce46b353/113010?pid=1 - context.startActivity(intent) - } - } else { - ToastUtils.showShort("必须先开启[通知权限]") - } - }) - Divider() - } - - if (!canDrawOverlays) { - AuthCard(title = "悬浮窗权限", - desc = "用于后台提示,显示保存快照按钮等功能", - onAuthClick = { - val intent = Intent( - Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - ) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) - }) - Divider() - } - - if (gkdAccessRunning) { - TextSwitch(name = "服务开启", - desc = "保持服务开启,根据订阅规则匹配屏幕目标节点", - checked = store.enableService, - onCheckedChange = { - updateStorage( - storeFlow, store.copy( - enableService = it - ) - ) - }) - Divider() - } - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { - navController.navigate(ClickLogPageDestination) - } - .padding(10.dp, 5.dp), - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "点击记录", fontSize = 18.sp - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = "如误触可在此快速定位关闭规则", fontSize = 14.sp - ) - } - Icon(imageVector = Icons.Default.KeyboardArrowRight, contentDescription = null) - } - Divider() - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp, 5.dp) - ) { - Text(text = subsStatus, fontSize = 18.sp) - if (latestRecordDesc != null) { - Text( - text = "最近点击: $latestRecordDesc", fontSize = 14.sp - ) - } - } - } + } } diff --git a/app/src/main/java/li/songe/gkd/ui/GroupItemPage.kt b/app/src/main/java/li/songe/gkd/ui/GroupItemPage.kt new file mode 100644 index 0000000..2ffa566 --- /dev/null +++ b/app/src/main/java/li/songe/gkd/ui/GroupItemPage.kt @@ -0,0 +1,110 @@ +package li.songe.gkd.ui + +import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph +import li.songe.gkd.util.LocalNavController +import li.songe.gkd.util.Singleton.imageLoader +import li.songe.gkd.util.appInfoCacheFlow +import li.songe.gkd.util.subsIdToRawFlow + + +@RootNavGraph +@Destination +@Composable +fun GroupItemPage(subsInt: Long, appId: String, groupKey: Int) { + val navController = LocalNavController.current + val subsIdToRawState = subsIdToRawFlow.collectAsState() + val appRaw = remember { + subsIdToRawState.value[subsInt]?.apps?.first { a -> a.id == appId } + } + val group = remember { + appRaw?.groups?.find { g -> g.key == groupKey } + } + val appInfoCache by appInfoCacheFlow.collectAsState() + Box(modifier = Modifier.fillMaxSize()) { + TopAppBar( + navigationIcon = { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = null, + ) + } + }, + title = { + Text( + text = ((appInfoCache[appId]?.name ?: appRaw?.name + ?: appId) + "/" + (group?.name ?: "未知规则")) + ) + }, + actions = {}, + modifier = Modifier.zIndex(1f), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.5f) + ) + ) + if (group != null) { + val state = rememberPagerState { group.allExampleUrls.size } + HorizontalPager( + modifier = Modifier.fillMaxSize(), state = state + ) { p -> + val url = group.allExampleUrls.getOrNull(p) + if (url != null) { + SubcomposeAsyncImage( + model = ImageRequest.Builder(LocalContext.current).data(url) + .crossfade(DefaultDurationMillis).build(), + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + loading = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + CircularProgressIndicator(modifier = Modifier.size(50.dp)) + } + }, + error = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "加载失败", color = MaterialTheme.colorScheme.error) + } + }, + imageLoader = imageLoader + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/ui/HomePage.kt b/app/src/main/java/li/songe/gkd/ui/HomePage.kt index 339541a..d4f4e7c 100644 --- a/app/src/main/java/li/songe/gkd/ui/HomePage.kt +++ b/app/src/main/java/li/songe/gkd/ui/HomePage.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -35,7 +36,13 @@ fun HomePage() { val vm = hiltViewModel() val tab by vm.tabFlow.collectAsState() - Scaffold(bottomBar = { + Scaffold(topBar = { + TopAppBar(title = { + Text( + text = tab.label, + ) + }) + }, bottomBar = { NavigationBar { BottomNavItems.forEach { navItem -> NavigationBarItem(selected = tab == navItem, modifier = Modifier, onClick = { diff --git a/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt b/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt index 76a3b7b..f173e85 100644 --- a/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt @@ -1,57 +1,70 @@ package li.songe.gkd.ui -import android.graphics.BitmapFactory -import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.unit.dp -import com.blankj.utilcode.util.ToastUtils +import androidx.compose.ui.zIndex +import coil.compose.AsyncImage import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.withContext -import li.songe.gkd.util.LaunchedEffectTry +import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions @RootNavGraph @Destination(style = ProfileTransitions::class) @Composable fun ImagePreviewPage( - filePath: String?, + filePath: String, + title: String? = null, ) { - val (bitmap, setBitmap) = remember { - mutableStateOf(null) - } - LaunchedEffectTry { - if (filePath != null) { - setBitmap(withContext(IO) { BitmapFactory.decodeFile(filePath).asImageBitmap() }) - } else { - ToastUtils.showShort("图片加载失败") - } - } - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - if (bitmap != null) { - Image( - bitmap = bitmap, - contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .padding(0.dp) + val navController = LocalNavController.current + + Box(modifier = Modifier.fillMaxSize()) { + TopAppBar( + navigationIcon = { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = null, + ) + } + }, + title = { + if (title != null) { + Text(text = title) + } + }, + actions = {}, + modifier = Modifier.zIndex(1f), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.5f) + ) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + AsyncImage( + model = filePath, contentDescription = null, modifier = Modifier.fillMaxWidth() ) } } + } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt b/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt index 9c0ba9e..bf7d1eb 100644 --- a/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt @@ -18,15 +18,12 @@ import androidx.compose.material.icons.filled.KeyboardArrowRight import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.Divider -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -71,7 +68,6 @@ val settingsNav = BottomNavItem( label = "设置", icon = SafeR.ic_cog, route = "settings" ) -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsPage() { val context = LocalContext.current as MainActivity @@ -99,199 +95,189 @@ fun SettingsPage() { val checkUpdating by checkUpdatingFlow.collectAsState() - Scaffold(topBar = { - TopAppBar(title = { - Text( - text = "设置" - ) - }) - }, content = { contentPadding -> - Column( - modifier = Modifier - .verticalScroll( - state = rememberScrollState() + + Column( + modifier = Modifier.verticalScroll( + state = rememberScrollState() + ) + ) { + TextSwitch(name = "后台隐藏", + desc = "在[最近任务]界面中隐藏本应用", + checked = store.excludeFromRecents, + onCheckedChange = { + updateStorage( + storeFlow, store.copy( + excludeFromRecents = it + ) ) - .padding(0.dp, 10.dp) - .padding(contentPadding) - ) { + }) + Divider() - TextSwitch(name = "后台隐藏", - desc = "在[最近任务]界面中隐藏本应用", - checked = store.excludeFromRecents, - onCheckedChange = { - updateStorage( - storeFlow, store.copy( - excludeFromRecents = it - ) + TextSwitch(name = "无障碍前台", + desc = "添加前台透明悬浮窗,关闭可能导致不工作", + checked = store.enableAbFloatWindow, + onCheckedChange = { + updateStorage( + storeFlow, store.copy( + enableAbFloatWindow = it ) - }) - Divider() - - TextSwitch(name = "无障碍前台", - desc = "添加前台透明悬浮窗,关闭可能导致不工作", - checked = store.enableAbFloatWindow, - onCheckedChange = { - updateStorage( - storeFlow, store.copy( - enableAbFloatWindow = it - ) - ) - }) - Divider() - - TextSwitch(name = "点击提示", - desc = "触发点击时提示:[${store.clickToast}]", - checked = store.toastWhenClick, - modifier = Modifier.clickable { - showToastInputDlg = true - }, - onCheckedChange = { - updateStorage( - storeFlow, store.copy( - toastWhenClick = it - ) - ) - if (it && !Settings.canDrawOverlays(context)) { - ToastUtils.showShort("需要悬浮窗权限") - } - }) - Divider() - - Row(modifier = Modifier - .clickable { - showSubsIntervalDlg = true - } - .padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) { - Text( - modifier = Modifier.weight(1f), text = "自动更新订阅", fontSize = 18.sp ) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = updateTimeRadioOptions.find { it.second == store.updateSubsInterval }?.first - ?: store.updateSubsInterval.toString(), fontSize = 14.sp - ) - Icon( - imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more" - ) - } - } - Divider() + }) + Divider() - TextSwitch(name = "自动更新应用", - desc = "打开应用时自动检测是否存在新版本", - checked = store.autoCheckAppUpdate, - onCheckedChange = { - updateStorage( - storeFlow, store.copy( - autoCheckAppUpdate = it - ) + TextSwitch(name = "点击提示", + desc = "触发点击时提示:[${store.clickToast}]", + checked = store.toastWhenClick, + modifier = Modifier.clickable { + showToastInputDlg = true + }, + onCheckedChange = { + updateStorage( + storeFlow, store.copy( + toastWhenClick = it ) - }) - Divider() - - SettingItem(title = if (checkUpdating) "检查更新ing" else "检查更新", onClick = { - appScope.launchTry { - if (checkUpdatingFlow.value) return@launchTry - val newVersion = checkUpdate() - if (newVersion == null) { - ToastUtils.showShort("暂无更新") - } + ) + if (it && !Settings.canDrawOverlays(context)) { + ToastUtils.showShort("需要悬浮窗权限") } }) - Divider() + Divider() - Row(modifier = Modifier - .clickable { - showEnableDarkThemeDlg = true - } - .padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) { + Row(modifier = Modifier + .clickable { + showSubsIntervalDlg = true + } + .padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.weight(1f), text = "自动更新订阅", fontSize = 18.sp + ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { Text( - modifier = Modifier.weight(1f), text = "深色模式", fontSize = 18.sp + text = updateTimeRadioOptions.find { it.second == store.updateSubsInterval }?.first + ?: store.updateSubsInterval.toString(), fontSize = 14.sp ) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = darkThemeRadioOptions.find { it.second == store.enableDarkTheme }?.first - ?: store.enableDarkTheme.toString(), fontSize = 14.sp - ) - Icon( - imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more" + Icon( + imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more" + ) + } + } + Divider() + + TextSwitch(name = "自动更新应用", + desc = "打开应用时自动检测是否存在新版本", + checked = store.autoCheckAppUpdate, + onCheckedChange = { + updateStorage( + storeFlow, store.copy( + autoCheckAppUpdate = it ) + ) + }) + Divider() + + SettingItem(title = if (checkUpdating) "检查更新ing" else "检查更新", onClick = { + appScope.launchTry { + if (checkUpdatingFlow.value) return@launchTry + val newVersion = checkUpdate() + if (newVersion == null) { + ToastUtils.showShort("暂无更新") } } - Divider() - Row(modifier = Modifier - .clickable { - showEnableGroupDlg = true - } - .padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) { - Text( - modifier = Modifier.weight(1f), text = "规则启用", fontSize = 18.sp - ) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = enableGroupRadioOptions.find { it.second == store.enableGroup }?.first - ?: store.enableGroup.toString(), fontSize = 14.sp - ) - Icon( - imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more" - ) - } - } - Divider() + }) + Divider() - TextSwitch(name = "保存日志", - desc = "保存最近7天的日志,大概占用您5M的空间", - checked = store.log2FileSwitch, - onCheckedChange = { - updateStorage( - storeFlow, store.copy( - log2FileSwitch = it - ) + Row(modifier = Modifier + .clickable { + showEnableDarkThemeDlg = true + } + .padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.weight(1f), text = "深色模式", fontSize = 18.sp + ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = darkThemeRadioOptions.find { it.second == store.enableDarkTheme }?.first + ?: store.enableDarkTheme.toString(), fontSize = 14.sp + ) + Icon( + imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more" + ) + } + } + Divider() + Row(modifier = Modifier + .clickable { + showEnableGroupDlg = true + } + .padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.weight(1f), text = "规则启用", fontSize = 18.sp + ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = enableGroupRadioOptions.find { it.second == store.enableGroup }?.first + ?: store.enableGroup.toString(), fontSize = 14.sp + ) + Icon( + imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more" + ) + } + } + Divider() + + TextSwitch(name = "保存日志", + desc = "保存最近7天的日志,大概占用您5M的空间", + checked = store.log2FileSwitch, + onCheckedChange = { + updateStorage( + storeFlow, store.copy( + log2FileSwitch = it ) - if (!it) { - appScope.launchTry(Dispatchers.IO) { - val logFiles = LogUtils.getLogFiles() - if (logFiles.isNotEmpty()) { - logFiles.forEach { f -> - f.delete() - } - ToastUtils.showShort("已删除全部日志") + ) + if (!it) { + appScope.launchTry(Dispatchers.IO) { + val logFiles = LogUtils.getLogFiles() + if (logFiles.isNotEmpty()) { + logFiles.forEach { f -> + f.delete() } + ToastUtils.showShort("已删除全部日志") } } - }) - Divider() - - SettingItem(title = "分享日志", onClick = { - vm.viewModelScope.launchTry(Dispatchers.IO) { - val logFiles = LogUtils.getLogFiles() - if (logFiles.isNotEmpty()) { - showShareLogDlg = true - } else { - ToastUtils.showShort("暂无日志") - } } }) - Divider() + Divider() - SettingItem(title = "高级模式", onClick = { - navController.navigate(DebugPageDestination) - }) - Divider() + SettingItem(title = "分享日志", onClick = { + vm.viewModelScope.launchTry(Dispatchers.IO) { + val logFiles = LogUtils.getLogFiles() + if (logFiles.isNotEmpty()) { + showShareLogDlg = true + } else { + ToastUtils.showShort("暂无日志") + } + } + }) + Divider() - SettingItem(title = "关于", onClick = { - navController.navigate(AboutPageDestination) - }) + SettingItem(title = "高级模式", onClick = { + navController.navigate(DebugPageDestination) + }) + Divider() + + SettingItem(title = "关于", onClick = { + navController.navigate(AboutPageDestination) + }) + + Spacer(modifier = Modifier.height(40.dp)) + } - Spacer(modifier = Modifier.height(40.dp)) - } - }) if (showSubsIntervalDlg) { Dialog(onDismissRequest = { showSubsIntervalDlg = false }) { @@ -348,7 +334,8 @@ fun SettingsPage() { verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .selectable(selected = (option.second == store.enableDarkTheme), + .selectable( + selected = (option.second == store.enableDarkTheme), onClick = { updateStorage( storeFlow, @@ -388,7 +375,8 @@ fun SettingsPage() { verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .selectable(selected = (option.second == store.enableGroup), + .selectable( + selected = (option.second == store.enableGroup), onClick = { updateStorage( storeFlow, diff --git a/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt b/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt index e67fe41..8fa04fc 100644 --- a/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel @@ -139,10 +140,28 @@ fun SnapshotPage() { fontFamily = FontFamily.Monospace ) Spacer(modifier = Modifier.width(10.dp)) - Text(text = snapshot.appName ?: "") + Text( + text = snapshot.appName ?: snapshot.appId ?: snapshot.id.toString(), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + if (snapshot.activityId != null) { + val showActivityId = + if (snapshot.appId != null && snapshot.activityId.startsWith( + snapshot.appId + ) + ) { + snapshot.activityId.substring(snapshot.appId.length) + } else { + snapshot.activityId + } + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = showActivityId, overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) } - Spacer(modifier = Modifier.width(10.dp)) - Text(text = snapshot.activityId ?: "") } Divider() } @@ -180,7 +199,8 @@ fun SnapshotPage() { .clickable(onClick = scope.launchAsFn { navController.navigate( ImagePreviewPageDestination( - filePath = snapshotVal.screenshotFile.absolutePath + filePath = snapshotVal.screenshotFile.absolutePath, + title = snapshotVal.appName, ) ) selectedSnapshot = null diff --git a/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt b/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt index d1b431f..8378da2 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt @@ -1,5 +1,6 @@ package li.songe.gkd.ui +import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.webkit.URLUtil @@ -28,7 +29,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar import androidx.compose.material3.pullrefresh.PullRefreshIndicator import androidx.compose.material3.pullrefresh.pullRefresh import androidx.compose.material3.pullrefresh.rememberPullRefreshState @@ -76,6 +76,7 @@ val subsNav = BottomNavItem( label = "订阅", icon = SafeR.ic_link, route = "subscription" ) +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun SubsManagePage() { val context = LocalContext.current @@ -130,13 +131,6 @@ fun SubsManagePage() { }) Scaffold( - topBar = { - TopAppBar(title = { - Text( - text = "订阅", - ) - }) - }, floatingActionButton = { FloatingActionButton(onClick = { if (subItems.any { it.id == 0L }) { @@ -151,11 +145,10 @@ fun SubsManagePage() { ) } }, - ) { padding -> + ) { _ -> Box( modifier = Modifier .fillMaxSize() - .padding(padding) .pullRefresh(pullRefreshState, subItems.isNotEmpty()) ) { LazyColumn( @@ -261,15 +254,13 @@ fun SubsManagePage() { Divider() } if (menuSubItemVal.id != -2L) { - Text(text = "删除订阅", - modifier = Modifier - .clickable { - deleteSubItem = menuSubItemVal - menuSubItem = null - } - .fillMaxWidth() - .padding(16.dp), - color = MaterialTheme.colorScheme.error) + Text(text = "删除订阅", modifier = Modifier + .clickable { + deleteSubItem = menuSubItemVal + menuSubItem = null + } + .fillMaxWidth() + .padding(16.dp), color = MaterialTheme.colorScheme.error) } } } diff --git a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt index fd18319..b3199b5 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt @@ -109,7 +109,6 @@ fun SubsPage( focusRequester.requestFocus() } }) - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -137,8 +136,11 @@ fun SubsPage( }, actions = { if (showSearchBar) { IconButton(onClick = { - showSearchBar = false - vm.searchStrFlow.value = "" + if (vm.searchStrFlow.value.isEmpty()) { + showSearchBar = false + } else { + vm.searchStrFlow.value = "" + } }) { Icon(Icons.Outlined.Close, contentDescription = null) } diff --git a/app/src/main/java/li/songe/gkd/ui/SubsVm.kt b/app/src/main/java/li/songe/gkd/ui/SubsVm.kt index dfb374b..853db3a 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsVm.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsVm.kt @@ -15,6 +15,7 @@ import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.map import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow +import li.songe.gkd.util.subsItemsFlow import java.text.Collator import java.util.Locale import javax.inject.Inject @@ -23,8 +24,8 @@ import javax.inject.Inject class SubsVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() { private val args = SubsPageDestination.argsFrom(stateHandle) - val subsItemFlow = DbSet.subsItemDao.queryById(args.subsItemId) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) + val subsItemFlow = + subsItemsFlow.map(viewModelScope) { s -> s.find { v -> v.id == args.subsItemId } } private val appSubsConfigsFlow = DbSet.subsConfigDao.queryAppTypeConfig(args.subsItemId) .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) diff --git a/app/src/main/java/li/songe/gkd/util/ProfileTransitions.kt b/app/src/main/java/li/songe/gkd/util/ProfileTransitions.kt index 505f3f1..b9723e0 100644 --- a/app/src/main/java/li/songe/gkd/util/ProfileTransitions.kt +++ b/app/src/main/java/li/songe/gkd/util/ProfileTransitions.kt @@ -4,35 +4,26 @@ import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.navigation.NavBackStackEntry -import com.blankj.utilcode.util.ScreenUtils import com.ramcosta.composedestinations.spec.DestinationStyle object ProfileTransitions : DestinationStyle.Animated { - private const val durationMillis = 400 override fun AnimatedContentTransitionScope.enterTransition(): EnterTransition? { - return slideInHorizontally( - initialOffsetX = { ScreenUtils.getScreenWidth() }, animationSpec = tween(durationMillis) - ) + return slideInHorizontally(tween()) { it } } override fun AnimatedContentTransitionScope.exitTransition(): ExitTransition? { - return slideOutHorizontally( - targetOffsetX = { -ScreenUtils.getScreenWidth()/2 }, animationSpec = tween(durationMillis) - ) + return slideOutHorizontally(tween()) { -it } + fadeOut(tween()) } override fun AnimatedContentTransitionScope.popEnterTransition(): EnterTransition? { - return slideInHorizontally( - initialOffsetX = { -ScreenUtils.getScreenWidth()/2 }, animationSpec = tween(durationMillis) - ) + return slideInHorizontally(tween()) { -it } } override fun AnimatedContentTransitionScope.popExitTransition(): ExitTransition? { - return slideOutHorizontally( - targetOffsetX = { ScreenUtils.getScreenWidth() }, animationSpec = tween(durationMillis) - ) + return slideOutHorizontally(tween()) { it } } } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/util/Singleton.kt b/app/src/main/java/li/songe/gkd/util/Singleton.kt index 655caad..f9fc287 100644 --- a/app/src/main/java/li/songe/gkd/util/Singleton.kt +++ b/app/src/main/java/li/songe/gkd/util/Singleton.kt @@ -1,5 +1,10 @@ package li.songe.gkd.util +import android.os.Build +import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.disk.DiskCache import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.contentnegotiation.ContentNegotiation @@ -7,6 +12,7 @@ import io.ktor.http.ContentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import li.songe.gkd.app @OptIn(ExperimentalSerializationApi::class) object Singleton { @@ -39,4 +45,19 @@ object Singleton { } } + val imageLoader by lazy { + ImageLoader.Builder(app).components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + }.diskCache { + app.filesDir + app.getExternalFilesDir(null) + DiskCache.Builder() + .directory((app.externalCacheDir ?: app.cacheDir).resolve("imageCache")).build() + }.build() + } + } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c6dec5..ccd4e18 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -75,7 +75,7 @@ dependencyResolutionManagement { library("compose.tooling", "androidx.compose.ui:ui-tooling:$composeVersion") library("compose.junit4", "androidx.compose.ui:ui-test-junit4:$composeVersion") library("compose.material3", "androidx.compose.material3:material3:1.1.2") - library("compose.activity", "androidx.activity:activity-compose:1.7.2") + library("compose.activity", "androidx.activity:activity-compose:1.8.0-rc01") // https://github.com/Tencent/MMKV/blob/master/README_CN.md library("tencent.mmkv", "com.tencent:mmkv:1.3.1") @@ -186,6 +186,9 @@ dependencyResolutionManagement { ) library("destinations.ksp", "io.github.raamcosta.compose-destinations:ksp:1.9.54") + library("coil.compose", "io.coil-kt:coil-compose:2.5.0") + library("coil.gif", "io.coil-kt:coil-gif:2.5.0") + // https://github.com/aclassen/ComposeReorderable library("others.reorderable", "org.burnoutcrew.composereorderable:reorderable:0.9.6") }