feat: 支持显示 exampleUrls + 状态栏优化

This commit is contained in:
lisonge 2023-11-08 21:46:31 +08:00
parent 05ddbb23ce
commit 7357897ce2
17 changed files with 543 additions and 375 deletions

View File

@ -184,4 +184,7 @@ dependencies {
implementation(libs.others.reorderable)
implementation(libs.androidx.splashscreen)
implementation(libs.coil.compose)
implementation(libs.coil.gif)
}

View File

@ -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(

View File

@ -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)
}

View File

@ -82,6 +82,20 @@ data class SubscriptionRaw(
}
}
}
@IgnoredOnParcel
val allExampleUrls by lazy {
mutableListOf<String>().apply {
if (exampleUrls != null) {
addAll(exampleUrls)
}
rules.forEach { r ->
if (r.exampleUrls != null) {
addAll(r.exampleUrls)
}
}
}
}
}
@Serializable

View File

@ -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 = "复制规则组")
}
}
})
}

View File

@ -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
)
}
}
}
}
}

View File

@ -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
)
}
}
}
}
}

View File

@ -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<HomePageVm>()
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 = {

View File

@ -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<ImageBitmap?>(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()
)
}
}
}

View File

@ -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,

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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())

View File

@ -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<NavBackStackEntry>.enterTransition(): EnterTransition? {
return slideInHorizontally(
initialOffsetX = { ScreenUtils.getScreenWidth() }, animationSpec = tween(durationMillis)
)
return slideInHorizontally(tween()) { it }
}
override fun AnimatedContentTransitionScope<NavBackStackEntry>.exitTransition(): ExitTransition? {
return slideOutHorizontally(
targetOffsetX = { -ScreenUtils.getScreenWidth()/2 }, animationSpec = tween(durationMillis)
)
return slideOutHorizontally(tween()) { -it } + fadeOut(tween())
}
override fun AnimatedContentTransitionScope<NavBackStackEntry>.popEnterTransition(): EnterTransition? {
return slideInHorizontally(
initialOffsetX = { -ScreenUtils.getScreenWidth()/2 }, animationSpec = tween(durationMillis)
)
return slideInHorizontally(tween()) { -it }
}
override fun AnimatedContentTransitionScope<NavBackStackEntry>.popExitTransition(): ExitTransition? {
return slideOutHorizontally(
targetOffsetX = { ScreenUtils.getScreenWidth() }, animationSpec = tween(durationMillis)
)
return slideOutHorizontally(tween()) { it }
}
}

View File

@ -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()
}
}

View File

@ -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")
}