mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-15 19:22:26 +08:00
refactor: permission, refresh appList, divider
This commit is contained in:
parent
015af353d6
commit
6a9f413cd5
|
@ -226,4 +226,5 @@ dependencies {
|
|||
implementation(libs.exp4j)
|
||||
|
||||
implementation(libs.toaster)
|
||||
implementation(libs.permissions)
|
||||
}
|
|
@ -3,17 +3,20 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<!-- 低版本Android截屏 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- com.tencent.bugly -->
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" /> <!-- packageManager.getApplicationInfo -->
|
||||
|
||||
<!-- 国产ROM-获取应用列表权限 -->
|
||||
<uses-permission android:name="com.android.permission.GET_INSTALLED_APPS" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
@ -87,8 +90,7 @@
|
|||
android:name=".service.GkdAbService"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:stopWithTask="false">
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
|
@ -97,38 +99,40 @@
|
|||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/ab_desc" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".service.ManageService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="false" />
|
||||
<service
|
||||
android:name=".debug.ScreenshotService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection"
|
||||
android:stopWithTask="false" />
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
<service
|
||||
android:name=".service.ManageService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Display the running state of the application" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".debug.HttpService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="false" />
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Enable the HTTP server to provide external browser connections and debugging" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".debug.FloatingService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="false" />
|
||||
<service
|
||||
android:name=".service.ShizukuService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="false" />
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Display a screenshot button for users to actively save screen information." />
|
||||
</service>
|
||||
<service
|
||||
android:name=".debug.SnapshotTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_capture"
|
||||
android:label="@string/capture_label"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:stopWithTask="false">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.dylanc.activityresult.launcher.PickContentLauncher
|
||||
import com.dylanc.activityresult.launcher.RequestPermissionLauncher
|
||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -18,18 +17,18 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.composition.CompositionActivity
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.permission.AuthDialog
|
||||
import li.songe.gkd.permission.updatePermissionState
|
||||
import li.songe.gkd.service.ManageService
|
||||
import li.songe.gkd.service.updateLauncherAppId
|
||||
import li.songe.gkd.ui.NavGraphs
|
||||
import li.songe.gkd.ui.component.ConfirmDialog
|
||||
import li.songe.gkd.ui.theme.AppTheme
|
||||
import li.songe.gkd.util.AuthDialog
|
||||
import li.songe.gkd.util.LocalLauncher
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.LocalPickContentLauncher
|
||||
import li.songe.gkd.util.LocalRequestPermissionLauncher
|
||||
import li.songe.gkd.util.UpgradeDialog
|
||||
import li.songe.gkd.util.appInfoCacheFlow
|
||||
import li.songe.gkd.util.initOrResetAppInfoCache
|
||||
import li.songe.gkd.util.initFolder
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.storeFlow
|
||||
|
||||
|
@ -39,7 +38,6 @@ class MainActivity : CompositionActivity({
|
|||
useLifeCycleLog()
|
||||
val launcher = StartActivityLauncher(this)
|
||||
val pickContentLauncher = PickContentLauncher(this)
|
||||
val requestPermissionLauncher = RequestPermissionLauncher(this)
|
||||
|
||||
lifecycleScope.launch {
|
||||
storeFlow.map(lifecycleScope) { s -> s.excludeFromRecents }.collect {
|
||||
|
@ -56,12 +54,10 @@ class MainActivity : CompositionActivity({
|
|||
|
||||
setContent {
|
||||
val navController = rememberNavController()
|
||||
|
||||
AppTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalLauncher provides launcher,
|
||||
LocalPickContentLauncher provides pickContentLauncher,
|
||||
LocalRequestPermissionLauncher provides requestPermissionLauncher,
|
||||
LocalNavController provides navController
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
|
@ -85,13 +81,15 @@ class MainActivity : CompositionActivity({
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// https://github.com/gkd-kit/gkd/issues/543
|
||||
// ContextCompat.checkSelfPermission(app, Manifest.permission.QUERY_ALL_PACKAGES) always is GRANTED
|
||||
if (appInfoCacheFlow.value.count { e -> !e.value.isSystem && !e.value.hidden } < 16) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
initOrResetAppInfoCache()
|
||||
}
|
||||
// 每次切换页面更新记录桌面 appId
|
||||
updateLauncherAppId()
|
||||
|
||||
// 在某些机型由于未知原因创建失败, 在此保证每次界面切换都能重新检测创建
|
||||
appScope.launch(Dispatchers.IO) {
|
||||
initFolder()
|
||||
}
|
||||
|
||||
updatePermissionState()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
|
|
@ -2,15 +2,14 @@ package li.songe.gkd
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.service.updateLauncherAppId
|
||||
import li.songe.gkd.util.authActionFlow
|
||||
import li.songe.gkd.permission.authReasonFlow
|
||||
import li.songe.gkd.util.checkUpdate
|
||||
import li.songe.gkd.util.initFolder
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.logZipDir
|
||||
import li.songe.gkd.util.newVersionApkDir
|
||||
|
@ -20,12 +19,6 @@ import li.songe.gkd.util.updateSubscription
|
|||
|
||||
class MainViewModel : ViewModel() {
|
||||
init {
|
||||
// 在某些机型由于未知原因创建失败
|
||||
// 在此保证每次重新打开APP都能重新检测创建
|
||||
initFolder()
|
||||
|
||||
// 每次打开页面更新记录桌面 appId
|
||||
updateLauncherAppId()
|
||||
|
||||
val localSubsItem = SubsItem(
|
||||
id = -2, order = -2, mtime = System.currentTimeMillis()
|
||||
|
@ -63,6 +56,7 @@ class MainViewModel : ViewModel() {
|
|||
checkUpdate()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
LogUtils.d(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +65,6 @@ class MainViewModel : ViewModel() {
|
|||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
authActionFlow.value = null
|
||||
authReasonFlow.value = null
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ data class SubsItem(
|
|||
|
||||
) {
|
||||
|
||||
val isSafeRemote by lazy {
|
||||
private val isSafeRemote by lazy {
|
||||
if (updateUrl != null) {
|
||||
isSafeUrl(updateUrl)
|
||||
} else {
|
||||
|
@ -38,6 +38,16 @@ data class SubsItem(
|
|||
}
|
||||
}
|
||||
|
||||
val sourceText by lazy {
|
||||
if (id < 0) {
|
||||
"本地来源"
|
||||
} else if (isSafeRemote) {
|
||||
"可信来源"
|
||||
} else {
|
||||
"未知来源"
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeAssets() {
|
||||
deleteSubscription(id)
|
||||
DbSet.subsItemDao.delete(this)
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
package li.songe.gkd.permission
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import com.hjq.permissions.OnPermissionCallback
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class AuthReason(
|
||||
val text: String,
|
||||
val confirm: () -> Unit
|
||||
)
|
||||
|
||||
val authReasonFlow = MutableStateFlow<AuthReason?>(null)
|
||||
|
||||
@Composable
|
||||
fun AuthDialog() {
|
||||
val authAction = authReasonFlow.collectAsState().value
|
||||
if (authAction != null) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = "权限请求")
|
||||
},
|
||||
text = {
|
||||
Text(text = authAction.text)
|
||||
},
|
||||
onDismissRequest = { authReasonFlow.value = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
authReasonFlow.value = null
|
||||
authAction.confirm()
|
||||
}) {
|
||||
Text(text = "确认")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { authReasonFlow.value = null }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class PermissionResult {
|
||||
data object Granted : PermissionResult()
|
||||
data class Denied(val doNotAskAgain: Boolean) : PermissionResult()
|
||||
}
|
||||
|
||||
suspend fun asyncRequestPermission(
|
||||
context: Activity,
|
||||
permission: String,
|
||||
): PermissionResult {
|
||||
if (XXPermissions.isGranted(context, permission)) {
|
||||
return PermissionResult.Granted
|
||||
}
|
||||
return suspendCoroutine { continuation ->
|
||||
XXPermissions.with(context)
|
||||
.unchecked()
|
||||
.permission(permission)
|
||||
.request(object : OnPermissionCallback {
|
||||
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
|
||||
if (allGranted) {
|
||||
continuation.resume(PermissionResult.Granted)
|
||||
} else {
|
||||
continuation.resume(PermissionResult.Denied(false))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
|
||||
continuation.resume(PermissionResult.Denied(doNotAskAgain))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkOrRequestPermission(
|
||||
context: Activity,
|
||||
permissionState: PermissionState
|
||||
): Boolean {
|
||||
if (!permissionState.updateAndGet()) {
|
||||
val result = permissionState.request?.let { it(context) } ?: return false
|
||||
if (result is PermissionResult.Denied) {
|
||||
if (result.doNotAskAgain) {
|
||||
authReasonFlow.value = permissionState.reason
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
144
app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt
Normal file
144
app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt
Normal file
|
@ -0,0 +1,144 @@
|
|||
package li.songe.gkd.permission
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.hjq.permissions.Permission
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.updateAndGet
|
||||
import li.songe.gkd.app
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.shizuku.shizukuIsSafeOK
|
||||
import li.songe.gkd.util.appInfoCacheFlow
|
||||
import li.songe.gkd.util.initOrResetAppInfoCache
|
||||
import li.songe.gkd.util.launchTry
|
||||
|
||||
class PermissionState(
|
||||
val check: () -> Boolean,
|
||||
val request: (suspend (context: Activity) -> PermissionResult)? = null,
|
||||
/**
|
||||
* show it when user doNotAskAgain
|
||||
*/
|
||||
val reason: AuthReason? = null,
|
||||
) {
|
||||
val stateFlow = MutableStateFlow(check())
|
||||
fun updateAndGet(): Boolean {
|
||||
return stateFlow.updateAndGet { check() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSelfPermission(permission: String): Boolean {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
app,
|
||||
permission
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
val notificationState by lazy {
|
||||
PermissionState(
|
||||
check = {
|
||||
checkSelfPermission(Permission.POST_NOTIFICATIONS)
|
||||
},
|
||||
request = {
|
||||
asyncRequestPermission(it, Permission.POST_NOTIFICATIONS)
|
||||
},
|
||||
reason = AuthReason(
|
||||
text = "当前操作需要[通知权限]\n\n您需要前往应用权限设置打开此权限",
|
||||
confirm = {
|
||||
XXPermissions.startPermissionActivity(app, Permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val canQueryPkgState by lazy {
|
||||
PermissionState(
|
||||
check = {
|
||||
XXPermissions.isGranted(app, Permission.GET_INSTALLED_APPS)
|
||||
},
|
||||
request = {
|
||||
asyncRequestPermission(it, Permission.GET_INSTALLED_APPS)
|
||||
},
|
||||
reason = AuthReason(
|
||||
text = "当前操作需要[读取应用列表权限]\n\n您需要前往应用权限设置打开此权限",
|
||||
confirm = {
|
||||
XXPermissions.startPermissionActivity(app, Permission.GET_INSTALLED_APPS)
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val canDrawOverlaysState by lazy {
|
||||
PermissionState(
|
||||
check = {
|
||||
Settings.canDrawOverlays(app)
|
||||
},
|
||||
request = {
|
||||
// 无法直接请求悬浮窗权限
|
||||
if (!Settings.canDrawOverlays(app)) {
|
||||
PermissionResult.Denied(true)
|
||||
} else {
|
||||
PermissionResult.Granted
|
||||
}
|
||||
},
|
||||
reason = AuthReason(
|
||||
text = "当前操作需要[悬浮窗权限]\n\n您需要前往应用权限设置打开此权限",
|
||||
confirm = {
|
||||
XXPermissions.startPermissionActivity(app, Permission.SYSTEM_ALERT_WINDOW)
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val canSaveToAlbumState by lazy {
|
||||
PermissionState(
|
||||
check = {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
},
|
||||
request = {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
asyncRequestPermission(it, Permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
PermissionResult.Granted
|
||||
}
|
||||
},
|
||||
reason = AuthReason(
|
||||
text = "当前操作需要[写入外部存储权限]\n\n您需要前往应用权限设置打开此权限",
|
||||
confirm = {
|
||||
XXPermissions.startPermissionActivity(app, Permission.SYSTEM_ALERT_WINDOW)
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val shizukuOkState by lazy {
|
||||
PermissionState(
|
||||
check = { shizukuIsSafeOK() },
|
||||
)
|
||||
}
|
||||
|
||||
fun updatePermissionState() {
|
||||
arrayOf(
|
||||
notificationState,
|
||||
canDrawOverlaysState,
|
||||
canSaveToAlbumState,
|
||||
shizukuOkState
|
||||
).forEach { it.updateAndGet() }
|
||||
|
||||
if (canQueryPkgState.stateFlow.value != canQueryPkgState.updateAndGet() ||
|
||||
// https://github.com/gkd-kit/gkd/issues/543
|
||||
appInfoCacheFlow.value.count { e -> !e.value.isSystem && !e.value.hidden } < 16
|
||||
) {
|
||||
appScope.launchTry {
|
||||
initOrResetAppInfoCache()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package li.songe.gkd.service
|
||||
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
|
||||
class ShizukuService: CompositionService({
|
||||
|
||||
})
|
|
@ -4,7 +4,6 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -22,7 +21,6 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
|
@ -40,6 +38,7 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -62,11 +61,14 @@ import li.songe.gkd.appScope
|
|||
import li.songe.gkd.debug.FloatingService
|
||||
import li.songe.gkd.debug.HttpService
|
||||
import li.songe.gkd.debug.ScreenshotService
|
||||
import li.songe.gkd.permission.canDrawOverlaysState
|
||||
import li.songe.gkd.permission.checkOrRequestPermission
|
||||
import li.songe.gkd.permission.notificationState
|
||||
import li.songe.gkd.permission.shizukuOkState
|
||||
import li.songe.gkd.shizuku.CommandResult
|
||||
import li.songe.gkd.shizuku.newActivityTaskManager
|
||||
import li.songe.gkd.shizuku.newUserService
|
||||
import li.songe.gkd.shizuku.safeGetTasks
|
||||
import li.songe.gkd.shizuku.shizukuIsSafeOK
|
||||
import li.songe.gkd.ui.component.AuthCard
|
||||
import li.songe.gkd.ui.component.SettingItem
|
||||
import li.songe.gkd.ui.component.TextSwitch
|
||||
|
@ -74,9 +76,6 @@ import li.songe.gkd.ui.destinations.SnapshotPageDestination
|
|||
import li.songe.gkd.util.LocalLauncher
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.authActionFlow
|
||||
import li.songe.gkd.util.canDrawOverlaysAuthAction
|
||||
import li.songe.gkd.util.checkOrRequestNotifPermission
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
|
@ -84,7 +83,6 @@ import li.songe.gkd.util.navigate
|
|||
import li.songe.gkd.util.openUri
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import li.songe.gkd.util.toast
|
||||
import li.songe.gkd.util.usePollState
|
||||
import rikka.shizuku.Shizuku
|
||||
|
||||
@RootNavGraph
|
||||
|
@ -92,6 +90,7 @@ import rikka.shizuku.Shizuku
|
|||
@Composable
|
||||
fun DebugPage() {
|
||||
val context = LocalContext.current as MainActivity
|
||||
val scope = rememberCoroutineScope()
|
||||
val launcher = LocalLauncher.current
|
||||
val navController = LocalNavController.current
|
||||
val store by storeFlow.collectAsState()
|
||||
|
@ -123,8 +122,8 @@ fun DebugPage() {
|
|||
.padding(contentPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
val shizukuIsOk by usePollState { shizukuIsSafeOK() }
|
||||
if (!shizukuIsOk) {
|
||||
val shizukuOk by shizukuOkState.stateFlow.collectAsState()
|
||||
if (!shizukuOk) {
|
||||
AuthCard(title = "Shizuku授权",
|
||||
desc = "高级模式:准确识别界面ID,强制模拟点击",
|
||||
onAuthClick = {
|
||||
|
@ -135,56 +134,9 @@ fun DebugPage() {
|
|||
toast("Shizuku可能没有运行")
|
||||
}
|
||||
})
|
||||
HorizontalDivider()
|
||||
ShizukuFragment(false)
|
||||
} else {
|
||||
TextSwitch(name = "Shizuku-界面识别",
|
||||
desc = "更准确识别界面ID",
|
||||
checked = store.enableShizukuActivity,
|
||||
onCheckedChange = { enableShizuku ->
|
||||
if (enableShizuku) {
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
// 校验方法是否适配, 再允许使用 shizuku
|
||||
val tasks =
|
||||
newActivityTaskManager()?.safeGetTasks()?.firstOrNull()
|
||||
if (tasks != null) {
|
||||
storeFlow.value = store.copy(
|
||||
enableShizukuActivity = true
|
||||
)
|
||||
} else {
|
||||
toast("Shizuku-界面识别校验失败,无法使用")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
storeFlow.value = store.copy(
|
||||
enableShizukuActivity = false
|
||||
)
|
||||
}
|
||||
})
|
||||
HorizontalDivider()
|
||||
TextSwitch(
|
||||
name = "Shizuku-模拟点击",
|
||||
desc = "变更 clickCenter 为强制模拟点击",
|
||||
checked = store.enableShizukuClick,
|
||||
onCheckedChange = { enableShizuku ->
|
||||
if (enableShizuku) {
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
val service = newUserService()
|
||||
val result = service.userService.execCommand("input tap 0 0")
|
||||
service.destroy()
|
||||
if (json.decodeFromString<CommandResult>(result).code == 0) {
|
||||
storeFlow.update { it.copy(enableShizukuClick = true) }
|
||||
} else {
|
||||
toast("Shizuku-模拟点击校验失败,无法使用")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
storeFlow.value = store.copy(
|
||||
enableShizukuClick = false
|
||||
)
|
||||
}
|
||||
|
||||
})
|
||||
HorizontalDivider()
|
||||
ShizukuFragment()
|
||||
}
|
||||
|
||||
val httpServerRunning by HttpService.isRunning.collectAsState()
|
||||
|
@ -240,11 +192,11 @@ fun DebugPage() {
|
|||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Switch(
|
||||
checked = httpServerRunning,
|
||||
onCheckedChange = {
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@Switch
|
||||
}
|
||||
onCheckedChange = scope.launchAsFn<Boolean> {
|
||||
if (it) {
|
||||
if (!checkOrRequestPermission(context, notificationState)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
HttpService.start()
|
||||
} else {
|
||||
HttpService.stop()
|
||||
|
@ -252,14 +204,12 @@ fun DebugPage() {
|
|||
}
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
SettingItem(
|
||||
title = "HTTP服务端口-${store.httpServerPort}", imageVector = Icons.Default.Edit
|
||||
) {
|
||||
showPortDlg = true
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
TextSwitch(
|
||||
name = "保留内存订阅",
|
||||
|
@ -270,23 +220,21 @@ fun DebugPage() {
|
|||
autoClearMemorySubs = !it
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
SettingItem(title = "快照记录", onClick = {
|
||||
navController.navigate(SnapshotPageDestination)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
val screenshotRunning by ScreenshotService.isRunning.collectAsState()
|
||||
TextSwitch(
|
||||
name = "截屏服务",
|
||||
desc = "生成快照需要获取屏幕截图,Android11无需开启",
|
||||
checked = screenshotRunning,
|
||||
onCheckedChange = appScope.launchAsFn<Boolean> {
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
onCheckedChange = scope.launchAsFn<Boolean> {
|
||||
if (it) {
|
||||
if (!checkOrRequestPermission(context, notificationState)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
val mediaProjectionManager =
|
||||
context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val activityResult =
|
||||
|
@ -298,29 +246,28 @@ fun DebugPage() {
|
|||
ScreenshotService.stop()
|
||||
}
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
val floatingRunning by FloatingService.isRunning.collectAsState()
|
||||
TextSwitch(
|
||||
name = "悬浮窗服务",
|
||||
desc = "显示截屏按钮,便于用户主动保存快照",
|
||||
checked = floatingRunning
|
||||
) {
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@TextSwitch
|
||||
}
|
||||
if (it) {
|
||||
if (Settings.canDrawOverlays(context)) {
|
||||
checked = floatingRunning,
|
||||
onCheckedChange = scope.launchAsFn<Boolean> {
|
||||
if (it) {
|
||||
if (!checkOrRequestPermission(context, notificationState)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
if (!checkOrRequestPermission(context, canDrawOverlaysState)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
val intent = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
} else {
|
||||
authActionFlow.value = canDrawOverlaysAuthAction
|
||||
FloatingService.stop(context)
|
||||
}
|
||||
} else {
|
||||
FloatingService.stop(context)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
)
|
||||
|
||||
TextSwitch(
|
||||
name = "音量快照",
|
||||
desc = "当音量变化时,生成快照,如果悬浮窗按钮不工作,可以使用这个",
|
||||
|
@ -331,7 +278,6 @@ fun DebugPage() {
|
|||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
TextSwitch(
|
||||
name = "截屏快照",
|
||||
desc = "当用户截屏时保存快照(需手动替换快照图片),仅支持部分小米设备",
|
||||
|
@ -342,7 +288,6 @@ fun DebugPage() {
|
|||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
TextSwitch(
|
||||
name = "隐藏快照状态栏",
|
||||
desc = "当保存快照时,隐藏截图里的顶部状态栏高度区域",
|
||||
|
@ -407,4 +352,59 @@ fun DebugPage() {
|
|||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShizukuFragment(enabled: Boolean = true) {
|
||||
val store by storeFlow.collectAsState()
|
||||
TextSwitch(name = "Shizuku-界面识别",
|
||||
desc = "更准确识别界面ID",
|
||||
checked = store.enableShizukuActivity,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { enableShizuku ->
|
||||
if (enableShizuku) {
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
// 校验方法是否适配, 再允许使用 shizuku
|
||||
val tasks =
|
||||
newActivityTaskManager()?.safeGetTasks()?.firstOrNull()
|
||||
if (tasks != null) {
|
||||
storeFlow.value = store.copy(
|
||||
enableShizukuActivity = true
|
||||
)
|
||||
} else {
|
||||
toast("Shizuku-界面识别校验失败,无法使用")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
storeFlow.value = store.copy(
|
||||
enableShizukuActivity = false
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
TextSwitch(
|
||||
name = "Shizuku-模拟点击",
|
||||
desc = "变更 clickCenter 为强制模拟点击",
|
||||
checked = store.enableShizukuClick,
|
||||
enabled = enabled,
|
||||
onCheckedChange = { enableShizuku ->
|
||||
if (enableShizuku) {
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
val service = newUserService()
|
||||
val result = service.userService.execCommand("input tap 0 0")
|
||||
service.destroy()
|
||||
if (json.decodeFromString<CommandResult>(result).code == 0) {
|
||||
storeFlow.update { it.copy(enableShizukuClick = true) }
|
||||
} else {
|
||||
toast("Shizuku-模拟点击校验失败,无法使用")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
storeFlow.value = store.copy(
|
||||
enableShizukuClick = false
|
||||
)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -50,7 +48,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.blankj.utilcode.util.ClipboardUtils
|
||||
import com.blankj.utilcode.util.ImageUtils
|
||||
import com.blankj.utilcode.util.UriUtils
|
||||
import com.dylanc.activityresult.launcher.launchForResult
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
@ -59,12 +56,13 @@ import kotlinx.coroutines.withContext
|
|||
import li.songe.gkd.data.Snapshot
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.debug.SnapshotExt
|
||||
import li.songe.gkd.permission.canSaveToAlbumState
|
||||
import li.songe.gkd.permission.checkOrRequestPermission
|
||||
import li.songe.gkd.ui.destinations.ImagePreviewPageDestination
|
||||
import li.songe.gkd.util.IMPORT_BASE_URL
|
||||
import li.songe.gkd.util.LoadStatus
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.LocalPickContentLauncher
|
||||
import li.songe.gkd.util.LocalRequestPermissionLauncher
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.format
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
|
@ -83,7 +81,6 @@ fun SnapshotPage() {
|
|||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
val pickContentLauncher = LocalPickContentLauncher.current
|
||||
val requestPermissionLauncher = LocalRequestPermissionLauncher.current
|
||||
|
||||
val vm = hiltViewModel<SnapshotVm>()
|
||||
val snapshots by vm.snapshotsState.collectAsState()
|
||||
|
@ -244,13 +241,8 @@ fun SnapshotPage() {
|
|||
text = "保存截图到相册",
|
||||
modifier = Modifier
|
||||
.clickable(onClick = vm.viewModelScope.launchAsFn {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
val isGranted =
|
||||
requestPermissionLauncher.launchForResult(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
if (!isGranted) {
|
||||
toast("保存失败,暂无权限")
|
||||
return@launchAsFn
|
||||
}
|
||||
if (!checkOrRequestPermission(context, canSaveToAlbumState)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
ImageUtils.save2Album(
|
||||
ImageUtils.getBitmap(snapshotVal.screenshotFile),
|
||||
|
|
|
@ -6,17 +6,26 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.util.formatTimeAgo
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.subsLoadErrorsFlow
|
||||
import li.songe.gkd.util.subsRefreshErrorsFlow
|
||||
|
||||
|
||||
@Composable
|
||||
|
@ -26,30 +35,28 @@ fun SubsItemCard(
|
|||
index: Int,
|
||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val subsLoadError by remember(subsItem.id) {
|
||||
subsLoadErrorsFlow.map(scope) { it[subsItem.id] }
|
||||
}.collectAsState()
|
||||
val subsRefreshError by remember(subsItem.id) {
|
||||
subsRefreshErrorsFlow.map(scope) { it[subsItem.id] }
|
||||
}.collectAsState()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (rawSubscription != null) {
|
||||
Row {
|
||||
Text(
|
||||
text = index.toString() + ". " + (rawSubscription.name),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = index.toString() + ". " + (rawSubscription.name),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row {
|
||||
val sourceText =
|
||||
if (subsItem.id < 0) {
|
||||
"本地来源"
|
||||
} else if (subsItem.isSafeRemote) {
|
||||
"可信来源"
|
||||
} else {
|
||||
"未知来源"
|
||||
}
|
||||
Text(text = sourceText, fontSize = 14.sp)
|
||||
Text(text = subsItem.sourceText, fontSize = 14.sp)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = formatTimeAgo(subsItem.mtime),
|
||||
|
@ -72,16 +79,34 @@ fun SubsItemCard(
|
|||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Text(
|
||||
text = rawSubscription.numText,
|
||||
fontSize = 14.sp
|
||||
text = rawSubscription.numText, fontSize = 14.sp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "本地无订阅文件,请下拉刷新",
|
||||
text = "${index}. id:${subsItem.id}",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
val color = if (subsLoadError != null) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
Text(
|
||||
text = subsLoadError?.message ?: "加载订阅中...",
|
||||
fontSize = 14.sp,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
if (subsRefreshError != null) {
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Text(
|
||||
text = "刷新错误: ${subsRefreshError?.message}",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
|
|
@ -20,6 +20,7 @@ fun TextSwitch(
|
|||
name: String = "",
|
||||
desc: String = "",
|
||||
checked: Boolean = true,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
|
@ -36,8 +37,9 @@ fun TextSwitch(
|
|||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Switch(
|
||||
checked,
|
||||
onCheckedChange,
|
||||
checked = checked,
|
||||
enabled = enabled,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,10 @@ import androidx.compose.material.icons.automirrored.filled.Sort
|
|||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material.icons.filled.Apps
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.PriorityHigh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
|
@ -58,14 +60,19 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.KeyboardUtils
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import li.songe.gkd.MainActivity
|
||||
import li.songe.gkd.permission.canQueryPkgState
|
||||
import li.songe.gkd.permission.checkOrRequestPermission
|
||||
import li.songe.gkd.ui.component.AppBarTextField
|
||||
import li.songe.gkd.ui.destinations.AppConfigPageDestination
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.SortTypeOption
|
||||
import li.songe.gkd.util.appRefreshingFlow
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.ruleSummaryFlow
|
||||
import li.songe.gkd.util.storeFlow
|
||||
|
@ -163,6 +170,17 @@ fun useAppListPage(): ScaffoldExt {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
val canQueryPkg by canQueryPkgState.stateFlow.collectAsState()
|
||||
if (!canQueryPkg) {
|
||||
IconButton(onClick = vm.viewModelScope.launchAsFn {
|
||||
checkOrRequestPermission(context, canQueryPkgState)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PriorityHigh,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = {
|
||||
showSearchBar = true
|
||||
}) {
|
||||
|
@ -245,114 +263,128 @@ fun useAppListPage(): ScaffoldExt {
|
|||
}
|
||||
}
|
||||
})
|
||||
}) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
state = listState
|
||||
) {
|
||||
items(orderedAppInfos, { it.id }) { appInfo ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
navController.navigate(AppConfigPageDestination(appInfo.id))
|
||||
}
|
||||
.height(IntrinsicSize.Min)
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (appInfo.icon != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f)
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(appInfo.icon),
|
||||
}
|
||||
) { padding ->
|
||||
val appRefreshing by appRefreshingFlow.collectAsState()
|
||||
if (!appRefreshing) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
state = listState
|
||||
) {
|
||||
items(orderedAppInfos, { it.id }) { appInfo ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
navController.navigate(AppConfigPageDestination(appInfo.id))
|
||||
}
|
||||
.height(IntrinsicSize.Min)
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (appInfo.icon != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.aspectRatio(1f)
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(appInfo.icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Android,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.aspectRatio(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Android,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = appInfo.name,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = LocalTextStyle.current.let {
|
||||
if (appInfo.isSystem) {
|
||||
it.copy(textDecoration = TextDecoration.Underline)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
)
|
||||
val appGroups = ruleSummary.appIdToAllGroups[appInfo.id] ?: emptyList()
|
||||
|
||||
val appDesc = if (appGroups.isNotEmpty()) {
|
||||
when (val disabledCount = appGroups.count { g -> !g.enable }) {
|
||||
0 -> {
|
||||
"${appGroups.size}组规则"
|
||||
}
|
||||
|
||||
appGroups.size -> {
|
||||
"${appGroups.size}组规则/${disabledCount}关闭"
|
||||
}
|
||||
|
||||
else -> {
|
||||
"${appGroups.size}组规则/${appGroups.size - disabledCount}启用/${disabledCount}关闭"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val desc = if (globalDesc != null) {
|
||||
if (appDesc != null) {
|
||||
"$globalDesc/$appDesc"
|
||||
} else {
|
||||
globalDesc
|
||||
}
|
||||
} else {
|
||||
appDesc
|
||||
}
|
||||
|
||||
if (desc != null) {
|
||||
Text(text = desc)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = "暂无规则",
|
||||
color = LocalContentColor.current.copy(alpha = 0.5f)
|
||||
text = appInfo.name,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = LocalTextStyle.current.let {
|
||||
if (appInfo.isSystem) {
|
||||
it.copy(textDecoration = TextDecoration.Underline)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
)
|
||||
val appGroups = ruleSummary.appIdToAllGroups[appInfo.id] ?: emptyList()
|
||||
|
||||
val appDesc = if (appGroups.isNotEmpty()) {
|
||||
when (val disabledCount = appGroups.count { g -> !g.enable }) {
|
||||
0 -> {
|
||||
"${appGroups.size}组规则"
|
||||
}
|
||||
|
||||
appGroups.size -> {
|
||||
"${appGroups.size}组规则/${disabledCount}关闭"
|
||||
}
|
||||
|
||||
else -> {
|
||||
"${appGroups.size}组规则/${appGroups.size - disabledCount}启用/${disabledCount}关闭"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val desc = if (globalDesc != null) {
|
||||
if (appDesc != null) {
|
||||
"$globalDesc/$appDesc"
|
||||
} else {
|
||||
globalDesc
|
||||
}
|
||||
} else {
|
||||
appDesc
|
||||
}
|
||||
|
||||
if (desc != null) {
|
||||
Text(text = desc)
|
||||
} else {
|
||||
Text(
|
||||
text = "暂无规则",
|
||||
color = LocalContentColor.current.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
if (orderedAppInfos.isEmpty() && searchStr.isNotEmpty()) {
|
||||
Text(
|
||||
text = "暂无搜索结果",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
if (orderedAppInfos.isEmpty() && searchStr.isNotEmpty()) {
|
||||
Text(
|
||||
text = "暂无搜索结果",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,9 +32,11 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import li.songe.gkd.MainActivity
|
||||
import li.songe.gkd.permission.checkOrRequestPermission
|
||||
import li.songe.gkd.permission.notificationState
|
||||
import li.songe.gkd.service.GkdAbService
|
||||
import li.songe.gkd.service.ManageService
|
||||
import li.songe.gkd.ui.component.AuthCard
|
||||
|
@ -43,13 +45,12 @@ import li.songe.gkd.ui.destinations.ClickLogPageDestination
|
|||
import li.songe.gkd.ui.destinations.SlowGroupPageDestination
|
||||
import li.songe.gkd.util.HOME_PAGE_URL
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.checkOrRequestNotifPermission
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.openUri
|
||||
import li.songe.gkd.util.ruleSummaryFlow
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import li.songe.gkd.util.tryStartActivity
|
||||
import li.songe.gkd.util.usePollState
|
||||
|
||||
val controlNav = BottomNavItem(label = "主页", icon = Icons.Outlined.Home)
|
||||
|
||||
|
@ -65,10 +66,6 @@ fun useControlPage(): ScaffoldExt {
|
|||
|
||||
val gkdAccessRunning by GkdAbService.isRunning.collectAsState()
|
||||
val manageRunning by ManageService.isRunning.collectAsState()
|
||||
val canDrawOverlays by usePollState { Settings.canDrawOverlays(context) }
|
||||
val canNotif by usePollState {
|
||||
NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||
}
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||
val scrollState = rememberScrollState()
|
||||
return ScaffoldExt(navItem = controlNav,
|
||||
|
@ -106,39 +103,15 @@ fun useControlPage(): ScaffoldExt {
|
|||
)
|
||||
})
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
if (!canNotif) {
|
||||
AuthCard(title = "通知权限",
|
||||
desc = "用于显示各类服务状态数据及前后台提示",
|
||||
onAuthClick = {
|
||||
checkOrRequestNotifPermission(context)
|
||||
})
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (!canDrawOverlays) {
|
||||
AuthCard(
|
||||
title = "悬浮窗权限",
|
||||
desc = "用于后台提示,显示保存快照按钮等功能",
|
||||
onAuthClick = {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.tryStartActivity(intent)
|
||||
})
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
TextSwitch(
|
||||
name = "常驻通知",
|
||||
desc = "通知栏显示运行状态及统计数据",
|
||||
checked = manageRunning && store.enableStatusService,
|
||||
onCheckedChange = {
|
||||
onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {
|
||||
if (it) {
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@TextSwitch
|
||||
if (!checkOrRequestPermission(context, notificationState)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
storeFlow.value = store.copy(
|
||||
enableStatusService = true
|
||||
|
@ -151,7 +124,6 @@ fun useControlPage(): ScaffoldExt {
|
|||
ManageService.stop(context)
|
||||
}
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
@ -178,7 +150,6 @@ fun useControlPage(): ScaffoldExt {
|
|||
contentDescription = null
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
@ -203,7 +174,6 @@ fun useControlPage(): ScaffoldExt {
|
|||
contentDescription = null
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
if (ruleSummary.slowGroupCount > 0) {
|
||||
Row(
|
||||
|
@ -230,9 +200,8 @@ fun useControlPage(): ScaffoldExt {
|
|||
contentDescription = null
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
package li.songe.gkd.ui.home
|
||||
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.EaseInSine
|
||||
import androidx.compose.animation.core.EaseOutSine
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -14,10 +22,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Autorenew
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
|
@ -27,6 +35,7 @@ import androidx.compose.material3.TextButton
|
|||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -34,6 +43,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -55,11 +65,10 @@ import li.songe.gkd.ui.destinations.AboutPageDestination
|
|||
import li.songe.gkd.ui.destinations.DebugPageDestination
|
||||
import li.songe.gkd.util.LoadStatus
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.authActionFlow
|
||||
import li.songe.gkd.util.buildLogFile
|
||||
import li.songe.gkd.util.canDrawOverlaysAuthAction
|
||||
import li.songe.gkd.util.checkUpdate
|
||||
import li.songe.gkd.util.checkUpdatingFlow
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.shareFile
|
||||
|
@ -94,7 +103,6 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
|
||||
val checkUpdating by checkUpdatingFlow.collectAsState()
|
||||
|
||||
|
||||
if (showSubsIntervalDlg) {
|
||||
Dialog(onDismissRequest = { showSubsIntervalDlg = false }) {
|
||||
Card(
|
||||
|
@ -335,7 +343,6 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
excludeFromRecents = it
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
TextSwitch(name = "前台悬浮窗",
|
||||
desc = "添加透明悬浮窗,关闭可能导致不点击/点击缓慢",
|
||||
|
@ -345,7 +352,6 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
enableAbFloatWindow = it
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
TextSwitch(name = "点击提示",
|
||||
desc = "触发点击时提示:[${store.clickToast}]",
|
||||
|
@ -354,15 +360,10 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
showToastInputDlg = true
|
||||
},
|
||||
onCheckedChange = {
|
||||
if (it && !Settings.canDrawOverlays(context)) {
|
||||
authActionFlow.value = canDrawOverlaysAuthAction
|
||||
return@TextSwitch
|
||||
}
|
||||
storeFlow.value = store.copy(
|
||||
toastWhenClick = it
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
Row(modifier = Modifier
|
||||
.clickable {
|
||||
|
@ -385,7 +386,6 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
TextSwitch(name = "自动更新应用",
|
||||
desc = "打开应用时自动检测是否存在新版本",
|
||||
|
@ -395,18 +395,27 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
autoCheckAppUpdate = it
|
||||
)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
SettingItem(title = if (checkUpdating) "检查更新ing" else "检查更新", onClick = {
|
||||
appScope.launchTry {
|
||||
if (checkUpdatingFlow.value) return@launchTry
|
||||
val newVersion = checkUpdate()
|
||||
if (newVersion == null) {
|
||||
toast("暂无更新")
|
||||
}
|
||||
}
|
||||
})
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = appScope.launchAsFn(Dispatchers.IO) {
|
||||
if (checkUpdatingFlow.value) return@launchAsFn
|
||||
val newVersion = checkUpdate()
|
||||
if (newVersion == null) {
|
||||
toast("暂无更新")
|
||||
}
|
||||
}
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp, 10.dp)
|
||||
.defaultMinSize(minHeight = 30.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "检查更新", fontSize = 18.sp)
|
||||
RotatingLoadingIcon(loading = checkUpdating)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier
|
||||
.clickable {
|
||||
|
@ -429,7 +438,6 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
TextSwitch(name = "保存日志",
|
||||
desc = "保存最近7天日志,关闭后无法定位解决错误",
|
||||
|
@ -451,17 +459,14 @@ fun useSettingsPage(): ScaffoldExt {
|
|||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
SettingItem(title = "分享日志", onClick = {
|
||||
showShareLogDlg = true
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
SettingItem(title = "高级模式", onClick = {
|
||||
navController.navigate(DebugPageDestination)
|
||||
})
|
||||
HorizontalDivider()
|
||||
|
||||
SettingItem(title = "关于", onClick = {
|
||||
navController.navigate(AboutPageDestination)
|
||||
|
@ -488,3 +493,34 @@ private val darkThemeRadioOptions = arrayOf(
|
|||
"启用" to true,
|
||||
"关闭" to false,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RotatingLoadingIcon(loading: Boolean) {
|
||||
val rotation = remember { Animatable(0f) }
|
||||
LaunchedEffect(loading) {
|
||||
if (loading) {
|
||||
rotation.animateTo(
|
||||
targetValue = rotation.value + 360f,
|
||||
animationSpec = tween(durationMillis = 500, easing = EaseInSine)
|
||||
)
|
||||
rotation.animateTo(
|
||||
targetValue = rotation.value + 360f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 500, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
)
|
||||
} else if (rotation.value != 0f) {
|
||||
rotation.animateTo(
|
||||
targetValue = rotation.value + 360f,
|
||||
animationSpec = tween(durationMillis = 500, easing = EaseOutSine)
|
||||
)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Autorenew,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.graphicsLayer(rotationZ = rotation.value)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package li.songe.gkd.ui.home
|
||||
|
||||
import android.webkit.URLUtil
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
@ -22,7 +20,6 @@ import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
|||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
|
@ -105,18 +102,6 @@ fun useSubsManagePage(): ScaffoldExt {
|
|||
val refreshing by subsRefreshingFlow.collectAsState()
|
||||
val pullRefreshState = rememberPullRefreshState(refreshing, { checkSubsUpdate(true) })
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyColumnState = rememberReorderableLazyColumnState(lazyListState) { from, to ->
|
||||
orderSubItems = orderSubItems.toMutableList().apply {
|
||||
add(to.index, removeAt(from.index))
|
||||
forEachIndexed { index, subsItem ->
|
||||
if (subsItem.order != index) {
|
||||
this[index] = subsItem.copy(order = index)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
menuSubItem?.let { menuSubItemVal ->
|
||||
Dialog(onDismissRequest = { menuSubItem = null }) {
|
||||
Card(
|
||||
|
@ -278,6 +263,18 @@ fun useSubsManagePage(): ScaffoldExt {
|
|||
}
|
||||
},
|
||||
) { padding ->
|
||||
val lazyListState = rememberLazyListState()
|
||||
val reorderableLazyColumnState =
|
||||
rememberReorderableLazyColumnState(lazyListState) { from, to ->
|
||||
orderSubItems = orderSubItems.toMutableList().apply {
|
||||
add(to.index, removeAt(from.index))
|
||||
forEachIndexed { index, subsItem ->
|
||||
if (subsItem.order != index) {
|
||||
this[index] = subsItem.copy(order = index)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
|
@ -290,16 +287,21 @@ fun useSubsManagePage(): ScaffoldExt {
|
|||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
itemsIndexed(orderSubItems, { _, subItem -> subItem.id }) { index, subItem ->
|
||||
ReorderableItem(reorderableLazyColumnState, key = subItem.id) { isDragging ->
|
||||
val width by animateDpAsState(
|
||||
if (isDragging) 1.dp else 0.dp,
|
||||
label = "width",
|
||||
)
|
||||
ReorderableItem(
|
||||
reorderableLazyColumnState,
|
||||
key = subItem.id,
|
||||
enabled = !refreshing,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Card(
|
||||
onClick = { menuSubItem = subItem },
|
||||
onClick = {
|
||||
if (!refreshing) {
|
||||
menuSubItem = subItem
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.longPressDraggableHandle(
|
||||
enabled = !refreshing,
|
||||
interactionSource = interactionSource,
|
||||
onDragStopped = {
|
||||
val changeItems = orderSubItems.filter { newItem ->
|
||||
|
@ -312,13 +314,8 @@ fun useSubsManagePage(): ScaffoldExt {
|
|||
}
|
||||
},
|
||||
)
|
||||
.animateItemPlacement()
|
||||
.padding(vertical = 3.dp, horizontal = 8.dp),
|
||||
elevation = CardDefaults.cardElevation(draggedElevation = 10.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
border = if (isDragging) BorderStroke(
|
||||
width, MaterialTheme.colorScheme.primary
|
||||
) else null,
|
||||
interactionSource = interactionSource,
|
||||
) {
|
||||
SubsItemCard(
|
||||
|
|
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.app
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.data.AppInfo
|
||||
|
@ -89,21 +90,27 @@ private fun updateAppInfo(appId: String) {
|
|||
}
|
||||
}
|
||||
|
||||
val appRefreshingFlow = MutableStateFlow(false)
|
||||
|
||||
suspend fun initOrResetAppInfoCache() {
|
||||
if (updateAppMutex.isLocked) return
|
||||
appRefreshingFlow.value = true
|
||||
updateAppMutex.withLock {
|
||||
val oldAppIds = appInfoCacheFlow.value.keys
|
||||
val appMap = appInfoCacheFlow.value.toMutableMap()
|
||||
app.packageManager.getInstalledPackages(0).forEach { packageInfo ->
|
||||
if (!oldAppIds.contains(packageInfo.packageName)) {
|
||||
val info = packageInfo.toAppInfo()
|
||||
if (info != null) {
|
||||
appMap[packageInfo.packageName] = info
|
||||
withContext(Dispatchers.IO) {
|
||||
app.packageManager.getInstalledPackages(0).forEach { packageInfo ->
|
||||
if (!oldAppIds.contains(packageInfo.packageName)) {
|
||||
val info = packageInfo.toAppInfo()
|
||||
if (info != null) {
|
||||
appMap[packageInfo.packageName] = info
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
appInfoCacheFlow.value = appMap.toImmutableMap()
|
||||
}
|
||||
appRefreshingFlow.value = false
|
||||
}
|
||||
|
||||
fun initAppState() {
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import li.songe.gkd.app
|
||||
|
||||
data class AuthAction(
|
||||
val title: String,
|
||||
val text: String,
|
||||
val confirm: () -> Unit
|
||||
)
|
||||
|
||||
private val notifAuthAction by lazy {
|
||||
AuthAction(
|
||||
title = "权限请求",
|
||||
text = "当前操作需要通知权限\n您需要前往[通知管理]打开此权限",
|
||||
confirm = {
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, app.packageName)
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, app.applicationInfo.uid)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
app.tryStartActivity(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val canDrawOverlaysAuthAction by lazy {
|
||||
AuthAction(
|
||||
title = "权限请求",
|
||||
text = "当前操作需要悬浮窗权限\n您需要前往[显示在其它应用的上层]打开此权限",
|
||||
confirm = {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
app.tryStartActivity(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val authActionFlow = MutableStateFlow<AuthAction?>(null)
|
||||
|
||||
@Composable
|
||||
fun AuthDialog() {
|
||||
val authAction = authActionFlow.collectAsState().value
|
||||
if (authAction != null) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = authAction.title)
|
||||
},
|
||||
text = {
|
||||
Text(text = authAction.text)
|
||||
},
|
||||
onDismissRequest = { authActionFlow.value = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
authActionFlow.value = null
|
||||
authAction.confirm()
|
||||
}) {
|
||||
Text(text = "确认")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { authActionFlow.value = null }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkOrRequestNotifPermission(context: Activity): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_DENIED
|
||||
) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
0 // TODO 如何感知 (adb shell pm grant)/appops 这类引起的授权变化?
|
||||
)
|
||||
} else {
|
||||
authActionFlow.value = notifAuthAction
|
||||
}
|
||||
return false
|
||||
} else if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) {
|
||||
authActionFlow.value = notifAuthAction
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,19 +1,8 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import com.dylanc.activityresult.launcher.PickContentLauncher
|
||||
import com.dylanc.activityresult.launcher.RequestPermissionLauncher
|
||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
val LocalLauncher =
|
||||
|
@ -21,27 +10,3 @@ val LocalLauncher =
|
|||
|
||||
val LocalPickContentLauncher =
|
||||
compositionLocalOf<PickContentLauncher> { error("not found LocalPickContentLauncher") }
|
||||
|
||||
val LocalRequestPermissionLauncher =
|
||||
compositionLocalOf<RequestPermissionLauncher> { error("not found RequestPermissionLauncher") }
|
||||
|
||||
@Composable
|
||||
fun <T> usePollState(
|
||||
context: CoroutineContext = Dispatchers.Default,
|
||||
interval: Long = 1000L,
|
||||
getter: () -> T,
|
||||
): MutableState<T> {
|
||||
val mutableState = remember { mutableStateOf(getter()) }
|
||||
LaunchedEffect(Unit) {
|
||||
withContext(context) {
|
||||
while (isActive) {
|
||||
delay(interval)
|
||||
mutableState.value = getter()
|
||||
}
|
||||
}
|
||||
}
|
||||
return mutableState
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -53,6 +53,8 @@ fun <T> CoroutineScope.launchAsFn(
|
|||
launch(context, start) {
|
||||
try {
|
||||
block(it)
|
||||
} catch (e: CancellationException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Toaster.show(e.message ?: e.stackTraceToString())
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.NetworkUtils
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -36,10 +38,41 @@ val subsItemsFlow by lazy {
|
|||
.stateIn(appScope, SharingStarted.Eagerly, persistentListOf())
|
||||
}
|
||||
|
||||
val subsIdToRawFlow by lazy {
|
||||
MutableStateFlow<ImmutableMap<Long, RawSubscription>>(persistentMapOf())
|
||||
data class SubsEntry(
|
||||
val subsItem: SubsItem,
|
||||
val subscription: RawSubscription?,
|
||||
) {
|
||||
val checkUpdateUrl = run {
|
||||
val checkUpdateUrl = subscription?.checkUpdateUrl ?: return@run null
|
||||
val updateUrl = subscription.updateUrl ?: subsItem.updateUrl ?: return@run checkUpdateUrl
|
||||
try {
|
||||
return@run URI(updateUrl).resolve(checkUpdateUrl).toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return@run null
|
||||
}
|
||||
}
|
||||
|
||||
val subsLoadErrorsFlow = MutableStateFlow<ImmutableMap<Long, Exception>>(persistentMapOf())
|
||||
val subsRefreshErrorsFlow = MutableStateFlow<ImmutableMap<Long, Exception>>(persistentMapOf())
|
||||
val subsIdToRawFlow = MutableStateFlow<ImmutableMap<Long, RawSubscription>>(persistentMapOf())
|
||||
|
||||
val subsEntriesFlow by lazy {
|
||||
combine(
|
||||
subsItemsFlow,
|
||||
subsIdToRawFlow,
|
||||
) { subsItems, subsIdToRaw ->
|
||||
subsItems.map { s ->
|
||||
SubsEntry(
|
||||
subsItem = s,
|
||||
subscription = subsIdToRaw[s.id],
|
||||
)
|
||||
}.toImmutableList()
|
||||
}.stateIn(appScope, SharingStarted.Eagerly, persistentListOf())
|
||||
}
|
||||
|
||||
|
||||
private val updateSubsFileMutex by lazy { Mutex() }
|
||||
fun updateSubscription(subscription: RawSubscription) {
|
||||
appScope.launchTry {
|
||||
|
@ -51,10 +84,19 @@ fun updateSubscription(subscription: RawSubscription) {
|
|||
newMap[subscription.id] = subscription
|
||||
}
|
||||
subsIdToRawFlow.value = newMap.toImmutableMap()
|
||||
if (subsLoadErrorsFlow.value.contains(subscription.id)) {
|
||||
subsLoadErrorsFlow.update {
|
||||
it.toMutableMap().apply {
|
||||
remove(subscription.id)
|
||||
}.toImmutableMap()
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
DbSet.subsItemDao.updateMtime(subscription.id, System.currentTimeMillis())
|
||||
subsFolder.resolve("${subscription.id}.json")
|
||||
.writeText(json.encodeToString(subscription))
|
||||
}
|
||||
LogUtils.d("更新订阅文件:id=${subscription.id},name=${subscription.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,33 +134,6 @@ fun getGroupRawEnable(
|
|||
} ?: group.enable ?: true
|
||||
}
|
||||
|
||||
data class SubsEntry(
|
||||
val subsItem: SubsItem,
|
||||
val subscription: RawSubscription?,
|
||||
) {
|
||||
val checkUpdateUrl = run {
|
||||
val checkUpdateUrl = subscription?.checkUpdateUrl ?: return@run null
|
||||
val updateUrl = subscription.updateUrl ?: subsItem.updateUrl ?: return@run checkUpdateUrl
|
||||
try {
|
||||
return@run URI(updateUrl).resolve(checkUpdateUrl).toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return@run null
|
||||
}
|
||||
}
|
||||
|
||||
val subsEntriesFlow by lazy {
|
||||
combine(subsItemsFlow, subsIdToRawFlow) { subsItems, subsIdToRaw ->
|
||||
subsItems.map { s ->
|
||||
SubsEntry(
|
||||
subsItem = s,
|
||||
subscription = subsIdToRaw[s.id]
|
||||
)
|
||||
}.toImmutableList()
|
||||
}.stateIn(appScope, SharingStarted.Eagerly, persistentListOf())
|
||||
}
|
||||
|
||||
data class RuleSummary(
|
||||
val globalRules: ImmutableList<GlobalRule> = persistentListOf(),
|
||||
val globalGroups: ImmutableList<ResolvedGlobalGroup> = persistentListOf(),
|
||||
|
@ -276,30 +291,44 @@ val ruleSummaryFlow by lazy {
|
|||
}.stateIn(appScope, SharingStarted.Eagerly, RuleSummary())
|
||||
}
|
||||
|
||||
private fun loadSubs(id: Long): RawSubscription {
|
||||
val file = subsFolder.resolve("${id}.json")
|
||||
if (!file.exists()) {
|
||||
error("订阅文件不存在")
|
||||
}
|
||||
val subscription = try {
|
||||
RawSubscription.parse(file.readText())
|
||||
} catch (e: Exception) {
|
||||
throw Exception("订阅文件解析失败", e)
|
||||
}
|
||||
if (subscription.id != id) {
|
||||
error("订阅文件id不一致")
|
||||
}
|
||||
return subscription
|
||||
}
|
||||
|
||||
private fun refreshRawSubsList(items: List<SubsItem>) {
|
||||
val subscriptions = subsIdToRawFlow.value.toMutableMap()
|
||||
val errors = subsLoadErrorsFlow.value.toMutableMap()
|
||||
items.forEach { s ->
|
||||
try {
|
||||
subscriptions[s.id] = loadSubs(s.id)
|
||||
errors.remove(s.id)
|
||||
} catch (e: Exception) {
|
||||
errors[s.id] = e
|
||||
}
|
||||
}
|
||||
subsIdToRawFlow.value = subscriptions.toImmutableMap()
|
||||
subsLoadErrorsFlow.value = errors.toImmutableMap()
|
||||
}
|
||||
|
||||
fun initSubsState() {
|
||||
subsItemsFlow.value
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
subsRefreshingFlow.value = true
|
||||
if (subsFolder.exists() && subsFolder.isDirectory) {
|
||||
updateSubsFileMutex.withLock {
|
||||
val filenames = DbSet.subsItemDao.queryAll().map { s -> "${s.id}.json" }
|
||||
val files =
|
||||
subsFolder.listFiles { f -> f.isFile && filenames.contains(f.name) }
|
||||
?: emptyArray()
|
||||
val subscriptions = files.mapNotNull { f ->
|
||||
try {
|
||||
RawSubscription.parse(f.readText())
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d("加载订阅文件失败", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
val newMap = subsIdToRawFlow.value.toMutableMap()
|
||||
subscriptions.forEach { s ->
|
||||
newMap[s.id] = s
|
||||
}
|
||||
subsIdToRawFlow.value = newMap.toImmutableMap()
|
||||
}
|
||||
updateSubsFileMutex.withLock {
|
||||
val items = DbSet.subsItemDao.queryAll()
|
||||
refreshRawSubsList(items)
|
||||
}
|
||||
subsRefreshingFlow.value = false
|
||||
}
|
||||
|
@ -308,77 +337,96 @@ fun initSubsState() {
|
|||
|
||||
private val updateSubsMutex by lazy { Mutex() }
|
||||
val subsRefreshingFlow = MutableStateFlow(false)
|
||||
|
||||
private suspend fun updateSubs(subsEntry: SubsEntry): RawSubscription? {
|
||||
val subsItem = subsEntry.subsItem
|
||||
val subsRaw = subsEntry.subscription
|
||||
if (subsItem.updateUrl == null || subsItem.id < 0) return null
|
||||
val checkUpdateUrl = subsEntry.checkUpdateUrl
|
||||
if (checkUpdateUrl != null && subsRaw != null) {
|
||||
try {
|
||||
val subsVersion = json.decodeFromString<SubsVersion>(
|
||||
json5ToJson(
|
||||
client.get(checkUpdateUrl).bodyAsText()
|
||||
)
|
||||
)
|
||||
LogUtils.d(
|
||||
"快速检测更新:id=${subsRaw.id},version=${subsRaw.version}",
|
||||
subsVersion
|
||||
)
|
||||
if (subsVersion.id == subsRaw.id && subsVersion.version <= subsRaw.version) {
|
||||
return null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d("快速检测更新失败", subsItem, e)
|
||||
}
|
||||
}
|
||||
val updateUrl = subsRaw?.updateUrl ?: subsItem.updateUrl
|
||||
val text = try {
|
||||
client.get(updateUrl).bodyAsText()
|
||||
} catch (e: Exception) {
|
||||
throw Exception("请求更新链接失败", e)
|
||||
}
|
||||
val newSubsRaw = try {
|
||||
RawSubscription.parse(text)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("解析文本失败", e)
|
||||
}
|
||||
if (newSubsRaw.id != subsItem.id) {
|
||||
error("新id=${newSubsRaw.id}不匹配旧id=${subsItem.id}")
|
||||
}
|
||||
if (subsRaw != null && newSubsRaw.version <= subsRaw.version) {
|
||||
LogUtils.d(
|
||||
"版本号不满足条件:id=${subsItem.id}",
|
||||
"${subsRaw.version} -> ${newSubsRaw.version}"
|
||||
)
|
||||
return null
|
||||
}
|
||||
return newSubsRaw
|
||||
}
|
||||
|
||||
fun checkSubsUpdate(showToast: Boolean = false) = appScope.launchTry(Dispatchers.IO) {
|
||||
if (updateSubsMutex.isLocked || subsRefreshingFlow.value) {
|
||||
return@launchTry
|
||||
}
|
||||
subsRefreshingFlow.value = true
|
||||
updateSubsMutex.withLock {
|
||||
if (subsRefreshingFlow.value) return@withLock
|
||||
subsRefreshingFlow.value = true
|
||||
LogUtils.d("开始检测更新")
|
||||
val subsEntries = subsEntriesFlow.value
|
||||
subsEntries.find { e -> e.subsItem.id == -2L && e.subscription == null }?.let { e ->
|
||||
updateSubscription(
|
||||
RawSubscription(
|
||||
id = e.subsItem.id,
|
||||
name = "本地订阅",
|
||||
version = 0
|
||||
)
|
||||
)
|
||||
if (!NetworkUtils.isAvailable()) {
|
||||
if (showToast) {
|
||||
toast("网络不可用")
|
||||
}
|
||||
return@withLock
|
||||
}
|
||||
LogUtils.d("开始检测更新")
|
||||
val localSubsEntries =
|
||||
subsEntriesFlow.value.filter { e -> e.subsItem.id < 0 && e.subscription == null }
|
||||
val subsEntries = subsEntriesFlow.value.filter { e -> e.subsItem.id >= 0 }
|
||||
refreshRawSubsList(localSubsEntries.map { e -> e.subsItem })
|
||||
|
||||
var successNum = 0
|
||||
subsEntries.forEach { subsEntry ->
|
||||
val subsItem = subsEntry.subsItem
|
||||
val subsRaw = subsEntry.subscription
|
||||
if (subsItem.updateUrl == null || subsItem.id < 0) return@forEach
|
||||
val checkUpdateUrl = subsEntry.checkUpdateUrl
|
||||
try {
|
||||
if (checkUpdateUrl != null && subsRaw != null) {
|
||||
try {
|
||||
val subsVersion = json.decodeFromString<SubsVersion>(
|
||||
json5ToJson(
|
||||
client.get(checkUpdateUrl).bodyAsText()
|
||||
)
|
||||
)
|
||||
LogUtils.d(
|
||||
"快速检测更新:id=${subsRaw.id},version=${subsRaw.version}",
|
||||
subsVersion
|
||||
)
|
||||
if (subsVersion.id == subsRaw.id && subsVersion.version <= subsRaw.version) {
|
||||
return@forEach
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d("快速检测更新失败", subsItem, e)
|
||||
val newSubsRaw = updateSubs(subsEntry)
|
||||
if (newSubsRaw != null) {
|
||||
updateSubscription(newSubsRaw)
|
||||
successNum++
|
||||
}
|
||||
if (subsRefreshErrorsFlow.value.contains(subsEntry.subsItem.id)) {
|
||||
subsRefreshErrorsFlow.update {
|
||||
it.toMutableMap().apply {
|
||||
remove(subsEntry.subsItem.id)
|
||||
}.toImmutableMap()
|
||||
}
|
||||
}
|
||||
val newSubsRaw = RawSubscription.parse(
|
||||
client.get(subsRaw?.updateUrl ?: subsItem.updateUrl).bodyAsText()
|
||||
)
|
||||
if (newSubsRaw.id != subsItem.id) {
|
||||
LogUtils.d("id不匹配", newSubsRaw.id, subsItem.id)
|
||||
return@forEach
|
||||
}
|
||||
if (subsRaw != null && newSubsRaw.version <= subsRaw.version) {
|
||||
LogUtils.d(
|
||||
"版本号不满足条件:id=${subsItem.id}",
|
||||
"${subsRaw.version} -> ${newSubsRaw.version}"
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
updateSubscription(newSubsRaw)
|
||||
DbSet.subsItemDao.update(
|
||||
subsItem.copy(
|
||||
mtime = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
successNum++
|
||||
LogUtils.d("更新订阅文件:id=${subsItem.id},name=${newSubsRaw.name}")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
subsRefreshErrorsFlow.update {
|
||||
it.toMutableMap().apply {
|
||||
set(subsEntry.subsItem.id, e)
|
||||
}.toImmutableMap()
|
||||
}
|
||||
LogUtils.d("检测更新失败", e)
|
||||
}
|
||||
}
|
||||
subsRefreshingFlow.value = false
|
||||
if (showToast) {
|
||||
if (successNum > 0) {
|
||||
toast("更新 $successNum 条订阅")
|
||||
|
@ -387,6 +435,7 @@ fun checkSubsUpdate(showToast: Boolean = false) = appScope.launchTry(Dispatchers
|
|||
}
|
||||
}
|
||||
LogUtils.d("结束检测更新")
|
||||
delay(500)
|
||||
}
|
||||
delay(500)
|
||||
subsRefreshingFlow.value = false
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.blankj.utilcode.util.AppUtils
|
||||
import com.blankj.utilcode.util.NetworkUtils
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.request.get
|
||||
|
@ -53,6 +54,9 @@ val newVersionFlow by lazy { MutableStateFlow<NewVersion?>(null) }
|
|||
val downloadStatusFlow by lazy { MutableStateFlow<LoadStatus<String>?>(null) }
|
||||
suspend fun checkUpdate(): NewVersion? {
|
||||
if (checkUpdatingFlow.value) return null
|
||||
if (!NetworkUtils.isAvailable()) {
|
||||
error("网络不可用")
|
||||
}
|
||||
checkUpdatingFlow.value = true
|
||||
try {
|
||||
val newVersion = client.get(UPDATE_URL).body<NewVersion>()
|
||||
|
|
|
@ -71,6 +71,7 @@ coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
|
|||
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "1.5.2" }
|
||||
exp4j = { module = "net.objecthunter:exp4j", version = "0.4.8" }
|
||||
toaster = { module = "com.github.getActivity:Toaster", version = "12.6" }
|
||||
permissions = { module = "com.github.getActivity:XXPermissions", version = "18.63" }
|
||||
|
||||
[plugins]
|
||||
android_library = { id = "com.android.library", version.ref = "android" }
|
||||
|
|
Loading…
Reference in New Issue
Block a user