diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt index a5272ad0..ff1126b2 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt @@ -112,6 +112,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val confirmDialog = rememberConfirmDialog() var lastClickTime by remember { mutableStateOf(0L) } + // 初始化缓存系统 + LaunchedEffect(Unit) { + viewModel.initializeCache(context) + } + // BottomSheet状态 val bottomSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true @@ -742,6 +747,7 @@ private fun ModuleList( if (success) { viewModel.fetchModuleList() + viewModel.markNeedRefresh() } if (!isUninstall) return val message = if (success) { @@ -889,6 +895,10 @@ fun ModuleItem( onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, onClick: (ModuleViewModel.ModuleInfo) -> Unit ) { + val context = LocalContext.current + val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) + val isHideTagRow = prefs.getBoolean("is_hide_tag_row", false) + ElevatedCard( colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), elevation = getCardElevation(), @@ -897,8 +907,10 @@ fun ModuleItem( val interactionSource = remember { MutableInteractionSource() } val indication = LocalIndication.current val viewModel = viewModel() - val moduleSize by remember(module.dirId) { - mutableStateOf(viewModel.getModuleSize(module.dirId)) + + // 使用缓存系统获取模块大小 + val sizeStr = remember(module.dirId) { + viewModel.getModuleSize(module.dirId) } Column( @@ -987,45 +999,47 @@ fun ModuleItem( Spacer(modifier = Modifier.height(12.dp)) // 标签行 - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - // 文件夹名称标签 - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.primary, - modifier = Modifier + if (!isHideTagRow) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() ) { - Text( - text = module.dirId, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), - color = MaterialTheme.colorScheme.onPrimary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + // 文件夹名称标签 + Surface( + shape = RoundedCornerShape(4.dp), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + ) { + Text( + text = module.dirId, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), + color = MaterialTheme.colorScheme.onPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + // 大小标签 + Surface( + shape = RoundedCornerShape(4.dp), + color = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier + ) { + Text( + text = sizeStr, + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), + color = MaterialTheme.colorScheme.onSecondaryContainer, + maxLines = 1 + ) + } } - // 大小标签 - Surface( - shape = RoundedCornerShape(4.dp), - color = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier - ) { - Text( - text = moduleSize, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), - color = MaterialTheme.colorScheme.onSecondaryContainer, - maxLines = 1 - ) - } + Spacer(modifier = Modifier.height(16.dp)) } - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider(thickness = Dp.Hairline) Spacer(modifier = Modifier.height(8.dp)) @@ -1060,7 +1074,7 @@ fun ModuleItem( interactionSource = interactionSource, contentPadding = ButtonDefaults.TextButtonContentPadding, - ) { + ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt index 16cd81ff..0766edaa 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt @@ -1,5 +1,6 @@ package com.sukisu.ultra.ui.viewmodel +import android.content.Context import android.os.SystemClock import android.util.Log import androidx.compose.runtime.derivedStateOf @@ -18,7 +19,6 @@ import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONObject import java.io.BufferedReader -import java.io.File import java.io.InputStreamReader import java.text.Collator import java.text.DecimalFormat @@ -26,6 +26,7 @@ import java.util.Locale import java.util.concurrent.TimeUnit import kotlin.math.log10 import kotlin.math.pow +import androidx.core.content.edit /** * @author ShirkNeko @@ -39,11 +40,38 @@ class ModuleViewModel : ViewModel() { private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0" } + // 模块大小缓存管理器 + private lateinit var moduleSizeCache: ModuleSizeCache + + fun initializeCache(context: Context) { + if (!::moduleSizeCache.isInitialized) { + moduleSizeCache = ModuleSizeCache(context) + } + } + fun getModuleSize(dirId: String): String { - val size = getModuleFolderSize(dirId) + if (!::moduleSizeCache.isInitialized) { + return "0 KB" + } + val size = moduleSizeCache.getModuleSize(dirId) return formatFileSize(size) } + /** + * 刷新所有模块的大小缓存 + * 只在安装、卸载、更新模块后调用 + */ + fun refreshModuleSizeCache() { + if (!::moduleSizeCache.isInitialized) return + + viewModelScope.launch(Dispatchers.IO) { + Log.d(TAG, "开始刷新模块大小缓存") + val currentModules = modules.map { it.dirId } + moduleSizeCache.refreshCache(currentModules) + Log.d(TAG, "模块大小缓存刷新完成") + } + } + class ModuleInfo( val id: String, val name: String, @@ -86,6 +114,8 @@ class ModuleViewModel : ViewModel() { fun markNeedRefresh() { isNeedRefresh = true + // 标记需要刷新时,同时刷新大小缓存 + refreshModuleSizeCache() } fun fetchModuleList() { @@ -155,6 +185,13 @@ class ModuleViewModel : ViewModel() { } } } + + // 首次加载模块列表时,初始化缓存 + if (::moduleSizeCache.isInitialized) { + val currentModules = modules.map { it.dirId } + moduleSizeCache.initializeCacheIfNeeded(currentModules) + } + isNeedRefresh = false }.onFailure { e -> Log.e(TAG, "fetchModuleList: ", e) @@ -233,6 +270,160 @@ class ModuleViewModel : ViewModel() { } } +/** + * 模块大小缓存管理器 + */ +class ModuleSizeCache(context: Context) { + companion object { + private const val TAG = "ModuleSizeCache" + private const val CACHE_PREFS_NAME = "module_size_cache" + private const val CACHE_VERSION_KEY = "cache_version" + private const val CACHE_INITIALIZED_KEY = "cache_initialized" + private const val CURRENT_CACHE_VERSION = 1 + } + + private val cachePrefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE) + private val sizeCache = mutableMapOf() + + init { + loadCacheFromPrefs() + } + + /** + * 从SharedPreferences加载缓存 + */ + private fun loadCacheFromPrefs() { + try { + val cacheVersion = cachePrefs.getInt(CACHE_VERSION_KEY, 0) + if (cacheVersion != CURRENT_CACHE_VERSION) { + Log.d(TAG, "缓存版本不匹配,清空缓存") + clearCache() + return + } + + val allEntries = cachePrefs.all + for ((key, value) in allEntries) { + if (key != CACHE_VERSION_KEY && key != CACHE_INITIALIZED_KEY && value is Long) { + sizeCache[key] = value + } + } + Log.d(TAG, "从缓存加载了 ${sizeCache.size} 个模块大小数据") + } catch (e: Exception) { + Log.e(TAG, "加载缓存失败", e) + clearCache() + } + } + + /** + * 保存缓存到SharedPreferences + */ + private fun saveCacheToPrefs() { + try { + cachePrefs.edit { + putInt(CACHE_VERSION_KEY, CURRENT_CACHE_VERSION) + putBoolean(CACHE_INITIALIZED_KEY, true) + + for ((dirId, size) in sizeCache) { + putLong(dirId, size) + } + + } + Log.d(TAG, "保存了 ${sizeCache.size} 个模块大小到缓存") + } catch (e: Exception) { + Log.e(TAG, "保存缓存失败", e) + } + } + + /** + * 获取模块大小(从缓存) + */ + fun getModuleSize(dirId: String): Long { + return sizeCache[dirId] ?: 0L + } + + /** + * 检查缓存是否已初始化,如果没有则初始化 + */ + fun initializeCacheIfNeeded(currentModules: List) { + val isInitialized = cachePrefs.getBoolean(CACHE_INITIALIZED_KEY, false) + if (!isInitialized || sizeCache.isEmpty()) { + Log.d(TAG, "首次初始化缓存,计算所有模块大小") + refreshCache(currentModules) + } else { + // 检查是否有新模块需要计算大小 + val newModules = currentModules.filter { !sizeCache.containsKey(it) } + if (newModules.isNotEmpty()) { + Log.d(TAG, "发现 ${newModules.size} 个新模块,计算大小: $newModules") + for (dirId in newModules) { + val size = calculateModuleFolderSize(dirId) + sizeCache[dirId] = size + Log.d(TAG, "新模块 $dirId 大小: ${formatFileSize(size)}") + } + saveCacheToPrefs() + } + } + } + + /** + * 刷新所有模块的大小缓存 + */ + fun refreshCache(currentModules: List) { + try { + // 清理不存在的模块缓存 + val toRemove = sizeCache.keys.filter { it !in currentModules } + toRemove.forEach { sizeCache.remove(it) } + + if (toRemove.isNotEmpty()) { + Log.d(TAG, "清理了 ${toRemove.size} 个不存在的模块缓存: $toRemove") + } + + // 计算所有当前模块的大小 + for (dirId in currentModules) { + val size = calculateModuleFolderSize(dirId) + sizeCache[dirId] = size + Log.d(TAG, "更新模块 $dirId 大小: ${formatFileSize(size)}") + } + + // 保存到持久化存储 + saveCacheToPrefs() + } catch (e: Exception) { + Log.e(TAG, "刷新缓存失败", e) + } + } + + /** + * 清空所有缓存 + */ + private fun clearCache() { + sizeCache.clear() + cachePrefs.edit { clear() } + Log.d(TAG, "清空所有缓存") + } + + /** + * 实际计算模块文件夹大小 + */ + private fun calculateModuleFolderSize(dirId: String): Long { + return try { + val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "du -sb /data/adb/modules/$dirId")) + val reader = BufferedReader(InputStreamReader(process.inputStream)) + val output = reader.readLine() + process.waitFor() + reader.close() + + if (output != null) { + val sizeStr = output.split("\t").firstOrNull() + sizeStr?.toLongOrNull() ?: 0L + } else { + 0L + } + } catch (e: Exception) { + Log.e(TAG, "计算模块大小失败 $dirId: ${e.message}") + 0L + } + } +} + /** * 格式化文件大小的工具函数 */ @@ -245,46 +436,4 @@ fun formatFileSize(bytes: Long): String { return DecimalFormat("#,##0.#").format( bytes / 1024.0.pow(digitGroups.toDouble()) ) + " " + units[digitGroups] -} - -/** - * 使用 su 权限调用 du 命令获取模块文件夹大小 - */ -fun getModuleFolderSize(dirId: String): Long { - return try { - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "du -sb /data/adb/modules/$dirId")) - val reader = BufferedReader(InputStreamReader(process.inputStream)) - val output = reader.readLine() - process.waitFor() - reader.close() - - if (output != null) { - val sizeStr = output.split("\t").firstOrNull() - sizeStr?.toLongOrNull() ?: 0L - } else { - 0L - } - } catch (e: Exception) { - Log.e("ModuleItem", "Error calculating module size with su for $dirId: ${e.message}") - 0L - } -} - -/** - * 递归计算目录大小(备用) - */ -private fun calculateDirectorySize(directory: File): Long { - var size = 0L - try { - directory.listFiles()?.forEach { file -> - size += if (file.isDirectory) { - calculateDirectorySize(file) - } else { - file.length() - } - } - } catch (e: Exception) { - Log.e("ModuleItem", "Error calculating directory size: ${e.message}") - } - return size } \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt index d31555f2..1d703267 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt @@ -233,6 +233,11 @@ fun MoreSettingsScreen( mutableStateOf(prefs.getBoolean("is_hide_link_card", false)) } + // 隐藏标签行开关状态 + var isHideTagRow by remember { + mutableStateOf(prefs.getBoolean("is_hide_tag_row", false)) + } + // SELinux状态 var selinuxEnabled by remember { mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing") @@ -315,6 +320,12 @@ fun MoreSettingsScreen( isHideLinkCard = newValue } + // 隐藏标签行开关状态 + val onHideTagRowChange = { newValue: Boolean -> + prefs.edit { putBoolean("is_hide_tag_row", newValue) } + isHideTagRow = newValue + } + // 备用图标开关状态 val onUseAltIconChange = { newValue: Boolean -> prefs.edit { putBoolean("use_alt_icon", newValue) } @@ -1080,6 +1091,15 @@ fun MoreSettingsScreen( checked = isHideLinkCard, onChange = onHideLinkCardChange ) + + // 隐藏标签行 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_tag_card), + summary = stringResource(R.string.hide_tag_card_summary), + checked = isHideTagRow, + onChange = onHideTagRowChange + ) } KsuIsValid { // 高级设置 diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 60795847..a5034b7b 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -195,6 +195,8 @@ 隐藏主页上的 SuSFS 状态信息 隐藏链接卡片 隐藏主页上的链接卡片信息 + 隐藏模块标签行 + 隐藏模块卡片中的文件夹名称和大小标签 主题模式 跟随系统 浅色 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 5edc358d..1086d2d8 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -197,6 +197,8 @@ Hide SuSFS status information on the home page Hide Link Card Status Hide link card information on the home page + Hide module label rows + Hide folder name and size labels in module cards Theme Follow system Light