├── .gitignore ├── .idea └── icon.png ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── qq.json │ ├── wechat.json │ └── xposed_init │ ├── java │ └── me │ │ └── kyuubiran │ │ └── qqcleaner │ │ ├── BaseActivity.kt │ │ ├── HookEntry.kt │ │ ├── MainActivity.kt │ │ ├── QQCleanerData.kt │ │ ├── TestActivity.kt │ │ ├── data │ │ └── CleanData.kt │ │ ├── hook │ │ ├── BaseHook.kt │ │ ├── ContextHook.kt │ │ └── EntryHook.kt │ │ ├── ui │ │ ├── QQCleanerApp.kt │ │ ├── composable │ │ │ ├── EditText.kt │ │ │ ├── Fab.kt │ │ │ ├── Item.kt │ │ │ ├── Line.kt │ │ │ ├── Switch.kt │ │ │ ├── TextField.kt │ │ │ ├── TopBar.kt │ │ │ └── dialog │ │ │ │ ├── BaseDialog.kt │ │ │ │ ├── BottomDialog.kt │ │ │ │ ├── ConfigAddDialog.kt │ │ │ │ ├── ConfigDialog.kt │ │ │ │ ├── Dialog.kt │ │ │ │ ├── DialogButton.kt │ │ │ │ ├── EditDialog.kt │ │ │ │ ├── FileAddDialog.kt │ │ │ │ ├── FileDialog.kt │ │ │ │ ├── SortAddDialog.kt │ │ │ │ ├── SortDialog.kt │ │ │ │ ├── ThemeDialog.kt │ │ │ │ ├── TimeDialog.kt │ │ │ │ └── view │ │ │ │ └── DialogBaseView.kt │ │ ├── scene │ │ │ ├── AboutScreen.kt │ │ │ ├── ConfigScreen.kt │ │ │ ├── DeveloperScreen.kt │ │ │ ├── MainScreen.kt │ │ │ ├── SortFixScreen.kt │ │ │ └── SortScreen.kt │ │ ├── theme │ │ │ ├── QQCleanerColors.kt │ │ │ ├── QQCleanerShapes.kt │ │ │ ├── QQCleanerTheme.kt │ │ │ └── QQCleanerTypes.kt │ │ └── util │ │ │ ├── ColorUtils.kt │ │ │ ├── Dp.kt │ │ │ ├── KeyboardUtils.kt │ │ │ ├── Shadow.kt │ │ │ ├── Shared.kt │ │ │ └── clickable.kt │ │ └── util │ │ ├── AutoCleanManager.kt │ │ ├── CleanManager.kt │ │ ├── ConfigManager.kt │ │ ├── DateUtils.kt │ │ ├── HostApp.kt │ │ ├── ImmersionBar.kt │ │ ├── PathUtil.kt │ │ ├── Utils.kt │ │ └── path │ │ ├── CommonPath.kt │ │ ├── QQPath.kt │ │ ├── TIMPath.kt │ │ └── WeChatPath.kt │ └── res │ ├── drawable-xxxhdpi │ ├── ic_developer_agoines.webp │ ├── ic_developer_ketal.webp │ ├── ic_developer_kitsunepie.webp │ ├── ic_developer_kyuubiran.webp │ ├── ic_developer_maitungtm.webp │ └── ic_developer_nextalone.webp │ ├── drawable │ ├── ic_a.xml │ ├── ic_add.xml │ ├── ic_android.xml │ ├── ic_back.xml │ ├── ic_chevron_right.xml │ ├── ic_chosen.xml │ ├── ic_cilpboard.xml │ ├── ic_close.xml │ ├── ic_copy.xml │ ├── ic_default.xml │ ├── ic_delete.xml │ ├── ic_edit.xml │ ├── ic_edit_name.xml │ ├── ic_file.xml │ ├── ic_home_qqcleaner.xml │ ├── ic_home_qqcleaner_dark.xml │ ├── ic_launcher_foreground.xml │ ├── ic_list_empty.xml │ ├── ic_list_empty_dark.xml │ ├── ic_moon.xml │ ├── ic_open.xml │ ├── ic_save.xml │ ├── ic_sun.xml │ ├── switch_default_off_to_on.xml │ ├── switch_default_off_to_on_drak.xml │ ├── switch_default_on_to_off.xml │ ├── switch_default_on_to_off_drak.xml │ ├── switch_off_to_on_white.xml │ ├── switch_off_to_on_white_drak.xml │ ├── switch_on_to_off_white.xml │ └── switch_on_to_off_white_drak.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ └── values │ ├── arrays.xml │ └── strings.xml ├── build.gradle.kts ├── clean-config ├── README.md ├── qq │ └── README.md └── wechat │ └── README.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image └── Project_QQCleaner.png └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/.idea/icon.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ProjectPicture](image/Project_QQCleaner.png) 2 | 3 | # 瘦身模块(QQCleaner) 4 | 5 | 轻量版本请点[这里](https://github.com/KyuubiRan/QQCleanerLite) 6 | 7 | [Telegram频道](https://t.me/QQCleaner) 8 | 9 | 注:本模块完全免费且开源,一切开发旨在学习,请勿用于非法用途。 10 | 11 | ## 使用方法 12 | 13 | 1.勾上模块 14 | 2.重启QQ/TIM/微信 15 | 3.设置->关于->QQ/TIM/微信瘦身 16 | 17 | ## 功能 18 | 19 | 1.自定义瘦身(导入/导出瘦身配置文件) 20 | 2.定时瘦身(自动执行瘦身操作) 21 | 22 | ### 2.0 TODO List 23 | 24 | - [x] 自定义瘦身 25 | - [x] 定时瘦身 26 | - [x] UI 27 | - [x] 适配 QQ 28 | - [x] 适配 TIM 29 | - [x] 适配 微信 30 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release/ 3 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | val composeVersion: String = "1.2.0-alpha07" 6 | 7 | android { 8 | compileSdk = 32 9 | buildToolsVersion = "32.0.0" 10 | namespace = "me.kyuubiran.qqcleaner" 11 | defaultConfig { 12 | applicationId = "me.kyuubiran.qqcleaner" 13 | minSdk = 26 14 | targetSdk = 32 15 | versionCode = 71 16 | versionName = "2.0.1" 17 | vectorDrawables { 18 | useSupportLibrary = true 19 | } 20 | } 21 | 22 | buildTypes { 23 | named("release") { 24 | isMinifyEnabled = true 25 | isShrinkResources = true 26 | setProguardFiles(listOf("proguard-rules.pro")) 27 | } 28 | } 29 | 30 | buildFeatures { 31 | compose = true 32 | } 33 | 34 | composeOptions { 35 | kotlinCompilerExtensionVersion = composeVersion 36 | } 37 | 38 | androidResources { 39 | additionalParameters("--preferred-density", "xxxhdpi") 40 | additionalParameters("--allow-reserved-package-id", "--package-id", "0x63") 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = "11" 45 | } 46 | compileOptions { 47 | sourceCompatibility = JavaVersion.VERSION_11 48 | targetCompatibility = JavaVersion.VERSION_11 49 | } 50 | 51 | packagingOptions { 52 | resources.excludes.addAll(listOf("META-INF/**", "kotlin/**", "google/**", "**.bin")) 53 | } 54 | 55 | dependenciesInfo { 56 | includeInApk = false 57 | } 58 | } 59 | 60 | dependencies { 61 | 62 | 63 | // implementation(files("./libs/EzXHelper-release.aar")) 64 | 65 | implementation("com.github.kyuubiran:EzXHelper:0.7.5") 66 | compileOnly("de.robv.android.xposed:api:82") 67 | 68 | implementation("androidx.compose.ui:ui:$composeVersion") 69 | implementation("androidx.compose.material:material:$composeVersion") 70 | // 导航 71 | implementation("androidx.navigation:navigation-compose:2.5.0-alpha04") 72 | // 虚拟键之类的适配工具 73 | implementation("com.google.accompanist:accompanist-insets:0.24.3-alpha") 74 | // 为了按钮添加的支持库 75 | implementation("androidx.compose.animation:animation-graphics:$composeVersion") 76 | } 77 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -repackageclasses "" 2 | -dontusemixedcaseclassnames 3 | 4 | -keep class me.kyuubiran.qqcleaner.HookEntry { 5 | (); 6 | } 7 | 8 | # 保留主类 9 | -keep public class me.kyuubiran.qqcleaner.MainActivity 10 | 11 | -keepattributes RuntimeVisible*Annotations 12 | 13 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 14 | public static void check*(...); 15 | public static void throw*(...); 16 | } 17 | 18 | # 去除 DebugMetadataKt() 注释 19 | -assumenosideeffects class kotlin.coroutines.jvm.internal.BaseContinuationImpl { 20 | java.lang.StackTraceElement getStackTraceElement() return null; 21 | } 22 | -assumenosideeffects public final class kotlin.coroutines.jvm.internal.DebugMetadataKt { 23 | private static final kotlin.coroutines.jvm.internal.DebugMetadata getDebugMetadataAnnotation(kotlin.coroutines.jvm.internal.BaseContinuationImpl) return null; 24 | } 25 | 26 | -allowaccessmodification 27 | -overloadaggressively -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 11 | 14 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/assets/wechat.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "默认配置", 3 | "author": "KyuubiRan", 4 | "enable": true, 5 | "hostApp": "wechat", 6 | "content": [ 7 | { 8 | "title": "缓存", 9 | "enable": true, 10 | "path": [ 11 | { 12 | "prefix": "!PublicDataDir", 13 | "suffix": "/cache" 14 | }, 15 | { 16 | "prefix": "!PublicDataDir", 17 | "suffix": "/MicroMsg/CDNTemp" 18 | }, 19 | { 20 | "prefix": "!PublicDataDir", 21 | "suffix": "/MicroMsg/FailMsgFileCache" 22 | }, 23 | { 24 | "prefix": "!PrivateDataDir", 25 | "suffix": "/cache/temp" 26 | }, 27 | { 28 | "prefix": "!PublicUserDataDir", 29 | "suffix": "/webcanvascache" 30 | } 31 | ] 32 | }, 33 | { 34 | "title": "图片", 35 | "enable": false, 36 | "path": [ 37 | { 38 | "prefix": "!PrivateUserDataDir", 39 | "suffix": "/image2" 40 | }, 41 | { 42 | "prefix": "!PrivateDataDir", 43 | "suffix": "/MicroMsg/tmpScanLicense" 44 | } 45 | ] 46 | }, 47 | { 48 | "title": "视频", 49 | "enable": false, 50 | "path": [ 51 | { 52 | "prefix": "!PublicUserDataDir", 53 | "suffix": "/video" 54 | }, 55 | { 56 | "prefix": "!PrivateDataDir", 57 | "suffix": "/cache/mv_video" 58 | } 59 | ] 60 | }, 61 | { 62 | "title": "头像", 63 | "enable": false, 64 | "path": [ 65 | { 66 | "prefix": "!PrivateUserDataDir", 67 | "suffix": "/avatar" 68 | } 69 | ] 70 | }, 71 | { 72 | "title": "小程序", 73 | "enable": false, 74 | "path": [ 75 | { 76 | "prefix": "!PrivateUserDataDir", 77 | "suffix": "/appbrand" 78 | }, 79 | { 80 | "prefix": "!PrivateDataDir", 81 | "suffix": "/MicroMsg/appbrand" 82 | } 83 | ] 84 | }, 85 | { 86 | "title": "红包皮肤", 87 | "enable": false, 88 | "path": [ 89 | { 90 | "prefix": "!PrivateDataDir", 91 | "suffix": "/MicroMsg/luckymoney" 92 | } 93 | ] 94 | }, 95 | { 96 | "title": "日志", 97 | "enable": false, 98 | "path": [ 99 | { 100 | "prefix": "!PublicDataDir", 101 | "suffix": "/MicroMsg/crash" 102 | }, 103 | { 104 | "prefix": "!PublicDataDir", 105 | "suffix": "/MicroMsg/xlog" 106 | }, 107 | { 108 | "prefix": "!PublicDataDir", 109 | "suffix": "/files/onelog" 110 | }, 111 | { 112 | "prefix": "!PublicDataDir", 113 | "suffix": "/files/tbslog" 114 | }, 115 | { 116 | "prefix": "!PublicDataDir", 117 | "suffix": "/files/Tencent/tbs_common_log" 118 | }, 119 | { 120 | "prefix": "!PublicDataDir", 121 | "suffix": "/files/Tencent/tbs_live_log" 122 | }, 123 | { 124 | "prefix": "!PrivateDataDir", 125 | "suffix": "/MicroMsg/crash" 126 | }, 127 | { 128 | "prefix": "!PrivateDataDir", 129 | "suffix": "/files/xlog" 130 | } 131 | ] 132 | }, 133 | { 134 | "title": "资源更新", 135 | "enable": false, 136 | "path": [ 137 | { 138 | "prefix": "!PublicDataDir", 139 | "suffix": "/MicroMsg/CheckResUpdate" 140 | } 141 | ] 142 | }, 143 | { 144 | "title": "X5内核", 145 | "enable": false, 146 | "path": [ 147 | { 148 | "prefix": "!PublicDataDir", 149 | "suffix": "/app_tbs" 150 | }, 151 | { 152 | "prefix": "!PrivateDataDir", 153 | "suffix": "/app_tbs" 154 | }, 155 | { 156 | "prefix": "!PublicDataDir", 157 | "suffix": "/app_tbs_64" 158 | }, 159 | { 160 | "prefix": "!PrivateDataDir", 161 | "suffix": "/app_tbs_64" 162 | }, 163 | { 164 | "prefix": "!PublicDataDir", 165 | "suffix": "/app_x5webview" 166 | }, 167 | { 168 | "prefix": "!PrivateDataDir", 169 | "suffix": "/app_x5webview" 170 | } 171 | ] 172 | } 173 | ] 174 | } -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | me.kyuubiran.qqcleaner.HookEntry -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner 2 | 3 | import android.content.res.Configuration 4 | import android.os.Bundle 5 | import android.view.Window.FEATURE_NO_TITLE 6 | import android.view.WindowManager 7 | import androidx.core.view.WindowCompat.setDecorFitsSystemWindows 8 | import me.kyuubiran.qqcleaner.QQCleanerData.isDark 9 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.Theme.* 10 | import me.kyuubiran.qqcleaner.util.* 11 | //open class BaseActivity : ComponentActivity() 12 | open class BaseActivity : TestActivity() { 13 | 14 | private val mLoder by lazy { BaseActivity::class.java.classLoader } 15 | 16 | override fun getClassLoader(): ClassLoader = mLoder 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | // 状态栏和导航栏延伸 20 | this.requestWindowFeature(FEATURE_NO_TITLE) 21 | setDecorFitsSystemWindows(window, false) 22 | // 输入法抬升 23 | @Suppress("DEPRECATION") 24 | this.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) 25 | // 这个是保证第一次打开的时候状态栏和导航栏变色 26 | // 用旧版的原因很简单, window.insetsController 在这个时候获取不到,所以这个 27 | setBarTranslation() 28 | isDark = when (QQCleanerData.theme) { 29 | Light -> false 30 | Dark -> true 31 | System -> isNightMode() 32 | } 33 | setLightOldMode(!isDark) 34 | super.onCreate(savedInstanceState) 35 | 36 | 37 | } 38 | 39 | override fun onRestoreInstanceState(savedInstanceState: Bundle) { 40 | val windowState = savedInstanceState.getBundle("android:viewHierarchyState") 41 | if (windowState != null) { 42 | windowState.classLoader = mLoder 43 | } 44 | super.onRestoreInstanceState(savedInstanceState) 45 | } 46 | 47 | /** 48 | * 设置导航栏和状态栏透明 49 | */ 50 | private fun setBarTranslation() { 51 | window.setStatusBarTranslation() 52 | window.setNavigationBarTranslation() 53 | } 54 | 55 | /** 56 | * 设置亮色导航栏和状态栏 57 | * @param enable 是否设置亮色导航栏和状态栏 58 | */ 59 | fun setLightMode(enable: Boolean = true) { 60 | statusBarLightMode(enable) 61 | navigationBarMode(enable) 62 | } 63 | 64 | /** 65 | * 设置亮色导航栏和状态栏(旧版本) 66 | * @param enable 是否设置亮色导航栏和状态栏 67 | */ 68 | private fun setLightOldMode(enable: Boolean = true) { 69 | statusBarLightOldMode(enable) 70 | navigationBarOldMode(enable) 71 | } 72 | 73 | /** 74 | * 判断当前是否是暗色模式 75 | * @return 当前是否是暗色模式 76 | */ 77 | private fun isNightMode(): Boolean { 78 | return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { 79 | Configuration.UI_MODE_NIGHT_YES -> true 80 | else -> false 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/HookEntry.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner 2 | 3 | import com.github.kyuubiran.ezxhelper.init.EzXHelperInit 4 | import de.robv.android.xposed.IXposedHookLoadPackage 5 | import de.robv.android.xposed.IXposedHookZygoteInit 6 | import de.robv.android.xposed.callbacks.XC_LoadPackage 7 | import me.kyuubiran.qqcleaner.hook.BaseHook 8 | import me.kyuubiran.qqcleaner.util.HostApp 9 | import me.kyuubiran.qqcleaner.util.hostApp 10 | import me.kyuubiran.qqcleaner.util.hostAppName 11 | 12 | class HookEntry : IXposedHookZygoteInit, IXposedHookLoadPackage { 13 | override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { 14 | EzXHelperInit.initZygote(startupParam) 15 | } 16 | 17 | private fun init(lpparam: XC_LoadPackage.LoadPackageParam, _hostApp: HostApp) { 18 | EzXHelperInit.initHandleLoadPackage(lpparam) 19 | hostApp = _hostApp 20 | EzXHelperInit.setLogTag("QQCleaner-${hostAppName}") 21 | EzXHelperInit.setToastTag("瘦身模块") 22 | BaseHook.initHooks() 23 | } 24 | 25 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { 26 | if (lpparam.packageName != lpparam.processName) return 27 | when (lpparam.packageName) { 28 | "com.tencent.mobileqq" -> { 29 | init(lpparam, HostApp.QQ) 30 | } 31 | "com.tencent.tim" -> { 32 | init(lpparam, HostApp.TIM) 33 | } 34 | "com.tencent.mm" -> { 35 | init(lpparam, HostApp.WE_CHAT) 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.compose.animation.AnimatedVisibility 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import com.google.accompanist.insets.ProvideWindowInsets 18 | import me.kyuubiran.qqcleaner.QQCleanerData.isDark 19 | import me.kyuubiran.qqcleaner.QQCleanerData.isFirst 20 | import me.kyuubiran.qqcleaner.ui.QQCleanerApp 21 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme 22 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTheme 23 | import me.kyuubiran.qqcleaner.ui.util.noClick 24 | 25 | class MainActivity : BaseActivity() { 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | 30 | // 这是设置布局 31 | setContent { 32 | QQCleanerTheme(QQCleanerData.theme) { 33 | ProvideWindowInsets { 34 | Box( 35 | Modifier 36 | .fillMaxSize() 37 | .noClick() 38 | .background(QQCleanerColorTheme.colors.appBarsAndItemBackgroundColor) 39 | ) { 40 | LaunchedEffect(isDark) { 41 | setLightMode(!isDark) 42 | } 43 | AnimatedVisibility(isFirst, modifier = Modifier.align(Alignment.Center)) { 44 | Image( 45 | modifier = Modifier 46 | .size(88.dp) 47 | .align(Alignment.Center), 48 | painter = painterResource( 49 | id = if (isDark) 50 | R.drawable.ic_home_qqcleaner_dark 51 | else 52 | R.drawable.ic_home_qqcleaner 53 | ), 54 | contentDescription = stringResource(id = R.string.icon_content_description), 55 | ) 56 | } 57 | QQCleanerApp() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/QQCleanerData.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.getCurrentTheme 7 | import me.kyuubiran.qqcleaner.util.ConfigManager.sIsBlackTheme 8 | 9 | object QQCleanerData { 10 | 11 | var theme by mutableStateOf(getCurrentTheme()) 12 | 13 | var isDark by mutableStateOf(false) 14 | 15 | var isBlack by mutableStateOf(sIsBlackTheme) 16 | 17 | var isFirst by mutableStateOf(true) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/TestActivity.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.text.TextUtils 6 | import android.view.ViewGroup 7 | import android.view.ViewGroup.LayoutParams 8 | import android.view.ViewGroup.LayoutParams.MATCH_PARENT 9 | import androidx.activity.OnBackPressedDispatcher 10 | import androidx.activity.OnBackPressedDispatcherOwner 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.CompositionContext 13 | import androidx.compose.ui.platform.ComposeView 14 | import androidx.lifecycle.* 15 | import androidx.savedstate.SavedStateRegistryController 16 | import androidx.savedstate.SavedStateRegistryOwner 17 | import androidx.savedstate.ViewTreeSavedStateRegistryOwner 18 | 19 | open class TestActivity : Activity(), LifecycleOwner, ViewModelStoreOwner, 20 | SavedStateRegistryOwner, OnBackPressedDispatcherOwner { 21 | // 需要自己实现 22 | private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) 23 | 24 | override fun getLifecycle(): Lifecycle { 25 | return lifecycleRegistry 26 | } 27 | 28 | 29 | private fun handleLifecycleEvent(event: Lifecycle.Event) = 30 | lifecycleRegistry.handleLifecycleEvent(event) 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | // 主要是让 ComponentActivity() 里面生命周期改成这样,应该就好了 35 | savedStateRegistryController.performRestore(null) 36 | handleLifecycleEvent(Lifecycle.Event.ON_RESUME) 37 | } 38 | 39 | fun setContent( 40 | parent: CompositionContext? = null, 41 | content: @Composable () -> Unit 42 | ) { 43 | val existingComposeView = window.decorView 44 | .findViewById(android.R.id.content) 45 | .getChildAt(0) as? ComposeView 46 | 47 | if (existingComposeView != null) with(existingComposeView) { 48 | setParentCompositionContext(parent) 49 | setContent(content) 50 | } else ComposeView(this).apply { 51 | // Set content and parent **before** setContentView 52 | // to have ComposeView create the composition on attach 53 | setParentCompositionContext(parent) 54 | setContent(content) 55 | // Set the view tree owners before setting the content view so that the inflation process 56 | // and attach listeners will see them already present 57 | setOwners() 58 | setContentView( 59 | this, LayoutParams( 60 | MATCH_PARENT, 61 | MATCH_PARENT 62 | ) 63 | ) 64 | } 65 | } 66 | 67 | 68 | /** 69 | * These owners are not set before AppCompat 1.3+ due to a bug, so we need to set them manually in 70 | * case developers are using an older version of AppCompat. 71 | */ 72 | private fun setOwners() { 73 | val decorView = window.decorView 74 | if (ViewTreeLifecycleOwner.get(decorView) == null) { 75 | ViewTreeLifecycleOwner.set(decorView, this) 76 | } 77 | if (ViewTreeViewModelStoreOwner.get(decorView) == null) { 78 | ViewTreeViewModelStoreOwner.set(decorView, this) 79 | } 80 | if (ViewTreeSavedStateRegistryOwner.get(decorView) == null) { 81 | ViewTreeSavedStateRegistryOwner.set(decorView, this) 82 | } 83 | } 84 | 85 | 86 | override fun onDestroy() { 87 | super.onDestroy() 88 | handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) 89 | } 90 | 91 | 92 | //ViewModelStore Methods 93 | private val store = ViewModelStore() 94 | 95 | override fun getViewModelStore(): ViewModelStore = store 96 | 97 | //SaveStateRegestry Methods 98 | 99 | private val savedStateRegistryController = SavedStateRegistryController.create(this) 100 | 101 | override val savedStateRegistry = savedStateRegistryController.savedStateRegistry 102 | 103 | // 这些是返回,没必要管它们 104 | private val mOnBackPressedDispatcher = OnBackPressedDispatcher { 105 | // Calling onBackPressed() on an Activity with its state saved can cause an 106 | // error on devices on API levels before 26. We catch that specific error and 107 | // throw all others. 108 | try { 109 | super.onBackPressed() 110 | } catch (e: IllegalStateException) { 111 | if (!TextUtils.equals( 112 | e.message, 113 | "Can not perform this action after onSaveInstanceState" 114 | ) 115 | ) { 116 | throw e 117 | } 118 | } 119 | } 120 | override fun getOnBackPressedDispatcher() = mOnBackPressedDispatcher 121 | override fun onBackPressed() { 122 | mOnBackPressedDispatcher.onBackPressed() 123 | } 124 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/hook/BaseHook.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.hook 2 | 3 | import com.github.kyuubiran.ezxhelper.utils.Log 4 | import com.github.kyuubiran.ezxhelper.utils.Log.logeIfThrow 5 | 6 | abstract class BaseHook { 7 | abstract val hookName: String 8 | protected var isInited = false 9 | 10 | abstract fun init() 11 | 12 | companion object { 13 | private val hooks: Array = arrayOf( 14 | ContextHook, 15 | EntryHook, 16 | ) 17 | 18 | fun initHooks() { 19 | hooks.forEach { 20 | runCatching { 21 | if (it.isInited) return@forEach 22 | it.init() 23 | it.isInited = true 24 | Log.i("Inited hook: ${it.hookName}") 25 | }.logeIfThrow("Failed to init hook: ${it.hookName}") 26 | } 27 | } 28 | } 29 | } 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/hook/ContextHook.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.hook 2 | 3 | import android.app.Application 4 | import com.github.kyuubiran.ezxhelper.init.EzXHelperInit 5 | import com.github.kyuubiran.ezxhelper.utils.* 6 | import de.robv.android.xposed.XC_MethodHook 7 | import me.kyuubiran.qqcleaner.BuildConfig 8 | import me.kyuubiran.qqcleaner.HookEntry 9 | import me.kyuubiran.qqcleaner.util.AutoCleanManager 10 | import me.kyuubiran.qqcleaner.util.hostApp 11 | import me.kyuubiran.qqcleaner.util.isQqOrTim 12 | import me.kyuubiran.qqcleaner.util.isWeChat 13 | 14 | object ContextHook : BaseHook() { 15 | override val hookName: String = "BaseContextHook" 16 | 17 | var unhook: XC_MethodHook.Unhook? = null 18 | 19 | override fun init() { 20 | when { 21 | hostApp.isQqOrTim -> { 22 | initQqOrTim() 23 | } 24 | hostApp.isWeChat -> { 25 | initWeChat() 26 | } 27 | } 28 | } 29 | 30 | private fun initQqOrTim() { 31 | unhook = getMethodByDesc("Lcom/tencent/mobileqq/startup/step/LoadDex;->doStep()Z") 32 | .hookAfter { 33 | unhook?.unhook() 34 | //获取Context 35 | val context = 36 | getFieldByDesc("Lcom/tencent/common/app/BaseApplicationImpl;->sApplication:Lcom/tencent/common/app/BaseApplicationImpl;") 37 | .getStaticNonNullAs() 38 | //初始化全局Context 39 | Log.i("Init Context") 40 | EzXHelperInit.initAppContext(context, addPath = true, initModuleResources = true) 41 | Log.i("Init ActivityProxyManager") 42 | EzXHelperInit.initActivityProxyManager( 43 | modulePackageName = BuildConfig.APPLICATION_ID, 44 | hostActivityProxyName = "com.tencent.mobileqq.activity.photo.CameraPreviewActivity", 45 | moduleClassLoader = HookEntry::class.java.classLoader!!, 46 | hostClassLoader = context.classLoader 47 | ) 48 | Log.i("Init ActivitySubActivity") 49 | EzXHelperInit.initSubActivity() 50 | 51 | AutoCleanManager.initAutoClean 52 | } 53 | } 54 | 55 | private fun initWeChat() { 56 | unhook = Application::class.java.getDeclaredMethod("onCreate").hookBefore { 57 | unhook?.unhook() 58 | val context = it.thisObject as Application 59 | //初始化全局Context 60 | Log.i("Init Context") 61 | // wechat的热修复Resources直接`addAssetPath`会失败 62 | EzXHelperInit.initAppContext(context, addPath = false) 63 | Log.i("Init ActivityProxyManager") 64 | EzXHelperInit.initActivityProxyManager( 65 | modulePackageName = BuildConfig.APPLICATION_ID, 66 | hostActivityProxyName = "com.tencent.mm.ui.contact.AddressUI", 67 | moduleClassLoader = HookEntry::class.java.classLoader!!, 68 | hostClassLoader = context.classLoader 69 | ) 70 | Log.i("Init ActivitySubActivity") 71 | EzXHelperInit.initSubActivity() 72 | 73 | AutoCleanManager.initAutoClean 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/hook/EntryHook.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.hook 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.BaseAdapter 10 | import android.widget.ListView 11 | import com.github.kyuubiran.ezxhelper.utils.* 12 | import com.github.kyuubiran.ezxhelper.utils.Log.logeIfThrow 13 | import me.kyuubiran.qqcleaner.MainActivity 14 | import me.kyuubiran.qqcleaner.util.hostApp 15 | import me.kyuubiran.qqcleaner.util.hostAppName 16 | import me.kyuubiran.qqcleaner.util.isQqOrTim 17 | import me.kyuubiran.qqcleaner.util.isWeChat 18 | import java.lang.reflect.Method 19 | 20 | object EntryHook : BaseHook() { 21 | override val hookName: String = "EntryHook" 22 | 23 | override fun init() { 24 | when { 25 | hostApp.isQqOrTim -> { 26 | initQqOrTim() 27 | } 28 | hostApp.isWeChat -> { 29 | initWeChat() 30 | } 31 | } 32 | } 33 | 34 | private fun startModuleSettingActivity(activity: Activity) { 35 | val intent = Intent(activity, MainActivity::class.java) 36 | activity.startActivity(intent) 37 | } 38 | 39 | private fun initQqOrTim() { 40 | getMethodByDesc("Lcom/tencent/mobileqq/activity/AboutActivity;->doOnCreate(Landroid/os/Bundle;)Z").also { m -> 41 | m.hookAfter { param -> 42 | val cFormSimpleItem = try { 43 | loadClass("com.tencent.mobileqq.widget.FormSimpleItem") 44 | } catch (e: Exception) { 45 | loadClass("com.tencent.mobileqq.widget.FormCommonSingleLineItem") 46 | } 47 | //获取ViewGroup 48 | val vg: ViewGroup = try { 49 | param.thisObject.getObjectAs("a", cFormSimpleItem) 50 | } catch (e: Exception) { 51 | param.thisObject.getObjectOrNullByType(cFormSimpleItem) as View 52 | }.parent as ViewGroup 53 | //创建入口 54 | val entry = cFormSimpleItem.newInstanceAs( 55 | args(param.thisObject), 56 | argTypes(Context::class.java) 57 | )!!.also { 58 | it.invokeMethod( 59 | "setLeftText", 60 | args("${hostAppName}瘦身"), 61 | argTypes(CharSequence::class.java) 62 | ) 63 | it.invokeMethod( 64 | "setRightText", 65 | args("芜狐~"), 66 | argTypes(CharSequence::class.java) 67 | ) 68 | } 69 | //设置点击事件 70 | entry.setOnClickListener { 71 | startModuleSettingActivity(param.thisObject as Activity) 72 | } 73 | //添加入口 74 | vg.addView(entry, 2) 75 | } 76 | } 77 | } 78 | 79 | private fun initWeChat() { 80 | runCatching { 81 | val actClass = try { 82 | loadClass("com.tencent.mm.plugin.setting.ui.setting.SettingsAboutMicroMsgUI") 83 | } catch (e: Exception) { 84 | // 旧版微信的关于界面 85 | loadClass("com.tencent.mm.ui.setting.SettingsAboutMicroMsgUI") 86 | } 87 | val preferenceClass = loadClass("com.tencent.mm.ui.base.preference.Preference") 88 | 89 | fun getKey(preference: Any): Any = preference.invokeMethod("getKey") 90 | ?: preference.getObject("mKey") 91 | 92 | actClass.getDeclaredMethod("onCreate", Bundle::class.java).hookAfter { 93 | val ctx = it.thisObject 94 | val listView = it.thisObject.invokeMethod("getListView") as? ListView 95 | ?: it.thisObject.getObjectAs("list", ListView::class.java) 96 | val adapter = listView.adapter as BaseAdapter 97 | val addMethod: Method = findMethod(adapter.javaClass) { 98 | returnType == Void.TYPE && parameterTypes.contentDeepEquals( 99 | arrayOf( 100 | preferenceClass, 101 | Int::class.java 102 | ) 103 | ) 104 | } 105 | // 构建一个入口 106 | val entry = loadClass("com.tencent.mm.ui.base.preference.IconPreference") 107 | .getConstructor(Context::class.java) 108 | .newInstance(ctx).apply { 109 | // 设置入口的属性 110 | invokeMethod( 111 | "setKey", 112 | args("QQCleaner"), 113 | argTypes(String::class.java) 114 | ) 115 | // 新版微信这里坏了 116 | invokeMethod( 117 | "setSummary", 118 | args("芜狐~"), 119 | argTypes(CharSequence::class.java) 120 | ) 121 | invokeMethod( 122 | "setTitle", 123 | args("微信瘦身"), 124 | argTypes(java.lang.CharSequence::class.java) 125 | ) 126 | } 127 | 128 | // 在adapter数据变化前添加entry 129 | findMethod(adapter.javaClass) { 130 | name == "notifyDataSetChanged" 131 | }.hookBefore { 132 | if (adapter.count == 0) return@hookBefore 133 | val position = adapter.count - 2 134 | if ("QQCleaner" != getKey(adapter.getItem(position))) { 135 | addMethod.invoke(adapter, entry, position) 136 | } 137 | } 138 | } 139 | 140 | // Hook Preference点击事件 141 | findMethod(actClass) { 142 | name == "onPreferenceTreeClick" 143 | && parameterTypes[1].isAssignableFrom(preferenceClass) 144 | }.hookBefore { 145 | if ("QQCleaner" == getKey(it.args[1])) { 146 | startModuleSettingActivity(it.thisObject as Activity) 147 | it.result = true 148 | } 149 | } 150 | }.logeIfThrow() 151 | } 152 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/QQCleanerApp.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui 2 | 3 | 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.compose.NavHost 6 | import androidx.navigation.compose.composable 7 | import androidx.navigation.compose.rememberNavController 8 | import me.kyuubiran.qqcleaner.QQCleanerData 9 | import me.kyuubiran.qqcleaner.ui.QQCleanerApp.About 10 | import me.kyuubiran.qqcleaner.ui.QQCleanerApp.Config 11 | import me.kyuubiran.qqcleaner.ui.QQCleanerApp.Developer 12 | import me.kyuubiran.qqcleaner.ui.QQCleanerApp.Main 13 | import me.kyuubiran.qqcleaner.ui.QQCleanerApp.Sort 14 | import me.kyuubiran.qqcleaner.ui.scene.* 15 | 16 | /** 17 | * QQCleaner App 的 UI 唯一入口点 18 | * 19 | * @author Agoines 20 | */ 21 | @Composable 22 | fun QQCleanerApp() { 23 | val navController = rememberNavController() 24 | NavHost( 25 | navController = navController, 26 | startDestination = Main 27 | ) { 28 | QQCleanerData.isFirst = false 29 | composable(Main) { 30 | MainScreen(navController = navController) 31 | } 32 | 33 | 34 | composable(Config) { 35 | ConfigScreen(navController = navController) 36 | navController.addOnDestinationChangedListener { _, destination, _ -> 37 | if (destination.route == Config) { 38 | 39 | } 40 | } 41 | } 42 | composable(Sort) { 43 | SortScreen(navController = navController) 44 | } 45 | // composable(SortFix) { 46 | // SortFixScreen(navController = navController) 47 | // } 48 | 49 | 50 | composable(About) { 51 | AboutScreen(navController = navController) 52 | } 53 | composable(Developer) { 54 | DeveloperScreen(navController = navController) 55 | } 56 | 57 | } 58 | 59 | } 60 | 61 | object QQCleanerApp { 62 | const val Config = "config_screen" 63 | const val Main = "main_screen" 64 | const val Developer = "developer_screen" 65 | const val Sort = "sort_screen" 66 | const val ConfigFix = "config_fix_screen" 67 | const val About = "about_screen" 68 | const val SortFix = "sort_fix_screen" 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/EditText.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.text.KeyboardOptions 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.input.key.KeyEvent 18 | import androidx.compose.ui.input.key.onKeyEvent 19 | import androidx.compose.ui.unit.dp 20 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 21 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes 22 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes 23 | 24 | @Composable 25 | fun EditText( 26 | modifier: Modifier, 27 | text: MutableState, 28 | onValueChange: (String) -> Unit, 29 | keyboardOptions: KeyboardOptions, 30 | onKeyEvent: KeyEvent.() -> Boolean, 31 | hintText: String 32 | ) { 33 | 34 | Box( 35 | // 这是居中,但是没什么必要 36 | contentAlignment = Alignment.Center, 37 | modifier = modifier 38 | .background( 39 | color = colors.typeBoxBackgroundColor, 40 | shape = QQCleanerShapes.dialogEditBackGround 41 | ) 42 | .padding( 43 | horizontal = 16.dp, 44 | vertical = 16.dp 45 | ), 46 | ) { 47 | TextField( 48 | modifier = Modifier 49 | .align(Alignment.Center) 50 | .fillMaxSize() 51 | .onKeyEvent { 52 | it.onKeyEvent() 53 | }, 54 | value = text.value, 55 | keyboardOptions = keyboardOptions, 56 | onValueChange = onValueChange 57 | ) 58 | val editHintColor by animateColorAsState( 59 | if (text.value.isEmpty()) 60 | colors.disableSecondTextColor else Color.Transparent, 61 | tween(100) 62 | ) 63 | 64 | Text( 65 | text = hintText, 66 | color = editHintColor, 67 | style = QQCleanerTypes.DialogEditStyle, 68 | modifier = Modifier 69 | .align(Alignment.Center) 70 | .fillMaxSize() 71 | ) 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/Fab.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.unit.dp 16 | import com.google.accompanist.insets.navigationBarsPadding 17 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 18 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes 19 | import me.kyuubiran.qqcleaner.ui.util.drawColoredShadow 20 | 21 | @Composable 22 | fun Fab(modifier: Modifier, text: String, onClick: () -> Unit) { 23 | Box( 24 | modifier = modifier 25 | .navigationBarsPadding() 26 | .padding(16.dp) 27 | .width(98.dp) 28 | .height(35.dp) 29 | 30 | .drawColoredShadow( 31 | colors.mainThemeColor, 32 | 0.7f, 33 | shadowRadius = 6.dp, 34 | offsetX = 0.dp, 35 | offsetY = (3).dp, 36 | ) 37 | .clip(RoundedCornerShape(80.dp)) 38 | .background( 39 | colors.mainThemeColor, 40 | ) 41 | .clickable { 42 | onClick() 43 | }, 44 | contentAlignment = Alignment.Center 45 | ) { 46 | Text( 47 | text = text, 48 | style = QQCleanerTypes.cleanerTextStyle, 49 | color = colors.whiteColor 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/Item.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.combinedClickable 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.unit.dp 16 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme 17 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes 18 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes 19 | 20 | @Composable 21 | fun Item(text: String, onClick: () -> Unit = {}, content: @Composable () -> Unit) { 22 | Row( 23 | modifier = Modifier 24 | .fillMaxWidth() 25 | .height(56.dp) 26 | .clip(QQCleanerShapes.cardGroupBackground) 27 | .clickable { onClick() } 28 | .padding(horizontal = 16.dp), 29 | verticalAlignment = Alignment.CenterVertically 30 | ) { 31 | Text( 32 | text = text, 33 | style = QQCleanerTypes.itemTextStyle, 34 | color = QQCleanerColorTheme.colors.secondTextColor, 35 | modifier = Modifier.weight(1f), 36 | ) 37 | content() 38 | } 39 | } 40 | 41 | @OptIn(ExperimentalFoundationApi::class) 42 | @Composable 43 | fun Item( 44 | text: String, 45 | onClick: () -> Unit = {}, 46 | onLongClick: () -> Unit = {}, 47 | content: @Composable () -> Unit 48 | ) { 49 | Row( 50 | modifier = Modifier 51 | .fillMaxWidth() 52 | .height(56.dp) 53 | .clip(QQCleanerShapes.cardGroupBackground) 54 | .combinedClickable( 55 | onClick = { 56 | onClick() 57 | }, 58 | onLongClick = { 59 | onLongClick() 60 | } 61 | ) 62 | .padding(horizontal = 16.dp), 63 | verticalAlignment = Alignment.CenterVertically 64 | ) { 65 | Text( 66 | text = text, 67 | style = QQCleanerTypes.itemTextStyle, 68 | color = QQCleanerColorTheme.colors.secondTextColor, 69 | modifier = Modifier.weight(1f), 70 | ) 71 | content() 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/Line.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 7 | 8 | @Composable 9 | fun Line(modifier: Modifier) { 10 | val dividerColor = colors.dividerColor 11 | Canvas( 12 | modifier = modifier 13 | ) { 14 | drawRect( 15 | color = dividerColor, 16 | size = this.size 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/Switch.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable 2 | 3 | import androidx.compose.animation.Crossfade 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi 6 | import androidx.compose.animation.graphics.res.animatedVectorResource 7 | import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter 8 | import androidx.compose.animation.graphics.vector.AnimatedImageVector 9 | import androidx.compose.foundation.Image 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.MutableState 12 | import me.kyuubiran.qqcleaner.QQCleanerData 13 | import me.kyuubiran.qqcleaner.R.drawable.* 14 | 15 | /** 16 | * 一个奇奇怪怪的小按钮 17 | * 18 | * @param checked 开关状态 19 | * @param isWhite 是否是白色按钮 20 | */ 21 | @OptIn(ExperimentalAnimationGraphicsApi::class) 22 | @Composable 23 | fun Switch( 24 | checked: MutableState, 25 | isWhite: Boolean = false 26 | ) { 27 | if (isWhite) { 28 | Crossfade(targetState = QQCleanerData.isDark, animationSpec = tween(600)) { 29 | val image = 30 | AnimatedImageVector.animatedVectorResource(if (it) switch_off_to_on_white_drak else switch_off_to_on_white) 31 | 32 | Image( 33 | painter = rememberAnimatedVectorPainter(image, checked.value), 34 | contentDescription = "按钮" 35 | ) 36 | } 37 | } else { 38 | Crossfade(targetState = QQCleanerData.isDark, animationSpec = tween(600)) { 39 | val image = 40 | AnimatedImageVector.animatedVectorResource(if (it) switch_default_off_to_on_drak else switch_default_off_to_on) 41 | Image( 42 | painter = rememberAnimatedVectorPainter(image, checked.value), 43 | contentDescription = "按钮" 44 | ) 45 | } 46 | 47 | } 48 | 49 | } 50 | 51 | @Composable 52 | fun SwitchItem( 53 | text: String, 54 | checked: MutableState, 55 | onClick: ((Boolean) -> Unit)? = null, 56 | onLongClick: () -> Unit = {}, 57 | clickNoToggle: Boolean = false 58 | ) { 59 | fun toggle() { 60 | if (!clickNoToggle) { 61 | checked.value = !checked.value 62 | if (onClick != null) { 63 | onClick(checked.value) 64 | } 65 | } 66 | } 67 | 68 | Item(text = text, onClick = { toggle() }, onLongClick = onLongClick) { 69 | Switch(checked = checked) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/TextField.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable 2 | 3 | import androidx.compose.foundation.text.BasicTextField 4 | import androidx.compose.foundation.text.KeyboardOptions 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.SolidColor 8 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 9 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes.DialogEditStyle 10 | 11 | @Composable 12 | fun TextField( 13 | modifier: Modifier, 14 | value: String, 15 | keyboardOptions: KeyboardOptions, 16 | onValueChange: (String) -> Unit 17 | ) { 18 | BasicTextField( 19 | textStyle = DialogEditStyle.copy(color = colors.secondTextColor), 20 | modifier = modifier, 21 | value = value, 22 | keyboardOptions = keyboardOptions, 23 | cursorBrush = SolidColor(colors.mainThemeColor), 24 | onValueChange = onValueChange 25 | ) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/TopBar.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.material.Icon 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.res.painterResource 14 | import androidx.compose.ui.unit.dp 15 | import me.kyuubiran.qqcleaner.R 16 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 17 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes 18 | 19 | @Composable 20 | fun TopBar(click: () -> Unit, titleText: String) { 21 | TopBar(click = click, titleText = titleText) { 22 | 23 | } 24 | } 25 | 26 | @Composable 27 | fun TopBar(backClick: () -> Unit, iconClick: () -> Unit, titleText: String, @DrawableRes id: Int) { 28 | TopBar(click = backClick, titleText = titleText) { 29 | Box( 30 | modifier = Modifier 31 | .size(56.dp) 32 | .clip(CircleShape) 33 | .clickable { 34 | iconClick() 35 | }, 36 | contentAlignment = Alignment.Center 37 | ) { 38 | Icon( 39 | painter = painterResource(id = id), 40 | contentDescription = "返回", 41 | modifier = Modifier.size(24.dp), 42 | tint = colors.secondTextColor 43 | ) 44 | } 45 | } 46 | } 47 | 48 | @Composable 49 | fun TopBar(click: () -> Unit, titleText: String, content: @Composable () -> Unit) { 50 | Row( 51 | verticalAlignment = Alignment.CenterVertically, 52 | modifier = Modifier 53 | .fillMaxWidth() 54 | .height(56.dp) 55 | .padding(end = 16.dp) 56 | ) { 57 | Box( 58 | modifier = Modifier 59 | .size(60.dp) 60 | .clip(CircleShape) 61 | .clickable { 62 | click() 63 | }, 64 | contentAlignment = Alignment.Center 65 | ) { 66 | Icon( 67 | painter = painterResource(id = R.drawable.ic_back), 68 | contentDescription = "返回", 69 | modifier = Modifier.size(24.dp), 70 | tint = colors.firstTextColor 71 | ) 72 | } 73 | 74 | Text( 75 | modifier = Modifier 76 | .weight(1f), 77 | style = QQCleanerTypes.TitleStyle, 78 | text = titleText, 79 | color = colors.firstTextColor 80 | ) 81 | content() 82 | 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/BaseDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import me.kyuubiran.qqcleaner.ui.composable.dialog.view.DialogBaseView 8 | 9 | open class BaseDialog(context: Context) { 10 | 11 | private var isBackPressBackDismiss: Boolean = true 12 | 13 | private val decorView: ViewGroup 14 | 15 | private val context: Context 16 | 17 | private lateinit var contentView: View 18 | 19 | private val dialogBaseView = DialogBaseView(context).apply { 20 | this.setFocusable() 21 | setOnBackPressed { 22 | if (isBackPressBackDismiss) { 23 | dismiss() 24 | } 25 | } 26 | } 27 | 28 | init { 29 | this.context = context 30 | this.decorView = (context as Activity).window.decorView as ViewGroup 31 | } 32 | 33 | fun setContentView(view: View) { 34 | this.contentView = view 35 | } 36 | 37 | fun show() { 38 | decorView.addView(dialogBaseView) 39 | decorView.addView(contentView) 40 | } 41 | 42 | open fun dismiss() { 43 | 44 | } 45 | 46 | fun removeView() { 47 | dialogBaseView.visibility = View.GONE 48 | decorView.removeView(dialogBaseView) 49 | decorView.removeView(contentView) 50 | } 51 | 52 | fun setDismissOnBackPress(isDismiss: Boolean) { 53 | this.isBackPressBackDismiss = isDismiss 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/BottomDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.app.Activity 4 | import androidx.compose.animation.Animatable 5 | import androidx.compose.animation.core.Animatable 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.foundation.shape.CircleShape 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Alignment.Companion.BottomStart 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.platform.LocalContext 21 | import androidx.compose.ui.platform.LocalDensity 22 | import androidx.compose.ui.res.painterResource 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.unit.Dp 25 | import androidx.compose.ui.unit.dp 26 | import com.google.accompanist.insets.LocalWindowInsets 27 | import kotlinx.coroutines.async 28 | import kotlinx.coroutines.awaitAll 29 | import me.kyuubiran.qqcleaner.R 30 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 31 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes 32 | import me.kyuubiran.qqcleaner.ui.util.hideKeyBoard 33 | import me.kyuubiran.qqcleaner.ui.util.noClick 34 | 35 | @Composable 36 | fun BottomDialog( 37 | onDismissRequest: () -> Unit, 38 | dialogHeight: Float, 39 | bottomHeight: Dp = 0.dp, 40 | state: MutableState = mutableStateOf(true), 41 | dialogText: String = "Dialog名字", 42 | content: @Composable ColumnScope.() -> Unit, 43 | ) { 44 | val background = colors.maskColor 45 | var flag by remember { mutableStateOf(true) } 46 | var isDismiss by remember { mutableStateOf(false) } 47 | val color = remember { Animatable(Color.Transparent) } 48 | val height = remember { Animatable(0f) } 49 | val context = LocalContext.current as Activity 50 | val insets = LocalWindowInsets.current 51 | 52 | val navigationHeight = with(LocalDensity.current) { insets.navigationBars.bottom.toDp() } 53 | // 通过外部修改 flag 控制 dialog 的关闭与否 54 | LaunchedEffect(state.value) { 55 | flag = state.value 56 | } 57 | 58 | Dialog( 59 | onRemoveViewRequest = { 60 | onDismissRequest() 61 | }, 62 | onDismissRequest = { 63 | flag = false 64 | }, 65 | isDismiss = isDismiss 66 | ) { 67 | // 判断当前状态来修改颜色和高度 68 | LaunchedEffect(flag) { 69 | listOf( 70 | // 为了并行而这么写的,具体能怎么改我不清楚 71 | async { 72 | // 颜色动画 73 | color.animateTo( 74 | targetValue = if (flag) background else Color.Transparent, 75 | animationSpec = tween(600) 76 | ) 77 | }, 78 | async { 79 | // 高度的动画 80 | height.animateTo( 81 | targetValue = if (flag) dialogHeight + navigationHeight.value else 0f, 82 | animationSpec = tween(600) 83 | ) 84 | } 85 | ).awaitAll() 86 | if (!flag) isDismiss = true 87 | } 88 | LaunchedEffect(dialogHeight) { 89 | if (flag) { 90 | height.animateTo( 91 | targetValue = dialogHeight + navigationHeight.value, 92 | animationSpec = tween(600) 93 | ) 94 | } 95 | } 96 | Box( 97 | modifier = Modifier 98 | .fillMaxSize() 99 | .padding(bottom = bottomHeight) 100 | .background(color = color.value) 101 | .noClick() 102 | ) { 103 | Column( 104 | modifier = Modifier 105 | .fillMaxWidth() 106 | .align(BottomStart) 107 | .height((height.value).dp) 108 | .background( 109 | shape = RoundedCornerShape(topEnd = 8.dp, topStart = 8.dp), 110 | color = colors.dialogBackgroundColor 111 | ) 112 | .padding(bottom = navigationHeight) 113 | ) { 114 | Row( 115 | Modifier 116 | .fillMaxWidth() 117 | .height(72.dp) 118 | .padding(start = 24.dp, end = 24.dp), 119 | verticalAlignment = Alignment.CenterVertically 120 | 121 | ) { 122 | Text( 123 | text = dialogText, 124 | modifier = Modifier 125 | .weight(1f), 126 | style = QQCleanerTypes.DialogTitleStyle, 127 | color = colors.firstTextColor 128 | ) 129 | // 图标 130 | Box( 131 | // 添加一个比较大的水波纹 132 | modifier = Modifier 133 | .size(48.dp) 134 | .clip(CircleShape) 135 | .clickable { 136 | flag = false 137 | // 这个是收回输入框 138 | context.hideKeyBoard() 139 | }, 140 | contentAlignment = Alignment.Center 141 | ) { 142 | Icon( 143 | modifier = Modifier 144 | .size(24.dp), 145 | tint = colors.firstTextColor, 146 | painter = painterResource(id = R.drawable.ic_close), 147 | contentDescription = stringResource( 148 | id = R.string.dialog_icon_clone 149 | ) 150 | ) 151 | } 152 | } 153 | content() 154 | } 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/ConfigAddDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.Icon 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.snapshots.SnapshotStateList 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import com.github.kyuubiran.ezxhelper.init.InitFields.moduleRes 22 | import com.github.kyuubiran.ezxhelper.utils.Log 23 | import com.github.kyuubiran.ezxhelper.utils.Log.logeIfThrow 24 | import me.kyuubiran.qqcleaner.R 25 | import me.kyuubiran.qqcleaner.R.string.cancel 26 | import me.kyuubiran.qqcleaner.R.string.dialog_title_config 27 | import me.kyuubiran.qqcleaner.data.CleanData 28 | import me.kyuubiran.qqcleaner.ui.composable.Line 29 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 30 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes.dialogButtonBackground 31 | 32 | 33 | @Composable 34 | fun ConfigAddDialog( 35 | list: SnapshotStateList, 36 | onDismissRequest: () -> Unit, 37 | ) { 38 | val state = remember { mutableStateOf(true) } 39 | BottomDialog( 40 | onDismissRequest = onDismissRequest, 41 | dialogHeight = if (list.isEmpty()) /* 408f else 352f */ 352f else 296f, 42 | dialogText = stringResource(id = dialog_title_config), 43 | state = state 44 | ) { 45 | // 线条绘制 46 | Line( 47 | Modifier 48 | .padding(top = 4.dp, start = 32.dp, end = 32.dp, bottom = 12.dp) 49 | .fillMaxWidth() 50 | .height(1.dp) 51 | ) 52 | 53 | ConfigItem( 54 | id = R.drawable.ic_cilpboard, 55 | text = stringResource(id = R.string.import_from_clipboard), 56 | onClick = { 57 | runCatching { 58 | CleanData.fromClipboard()!!.let { 59 | list.add(it) 60 | it.save() 61 | Log.toast( 62 | moduleRes.getString( 63 | R.string.import_from_clipboard_success, 64 | it.title 65 | ) 66 | ) 67 | } 68 | }.logeIfThrow { 69 | Log.toast(moduleRes.getString(R.string.import_from_clipboard_error)) 70 | } 71 | state.value = false 72 | } 73 | ) 74 | ConfigItem( 75 | id = R.drawable.ic_file, 76 | text = stringResource(id = R.string.import_from_file), 77 | onClick = { 78 | state.value = false 79 | } 80 | ) 81 | // ConfigItem( 82 | // id = R.drawable.ic_add, 83 | // text = stringResource(id = R.string.create_config), 84 | // onClick = { 85 | // //TODO("新建配置") 86 | // state.value = false 87 | // } 88 | // ) 89 | if (list.isEmpty()) { 90 | ConfigItem( 91 | id = R.drawable.ic_default, 92 | text = stringResource(id = R.string.create_default_config), 93 | onClick = { 94 | list.add(CleanData.createDefaultCleanData().also { it.save() }) 95 | Log.toast(moduleRes.getString(R.string.create_default_config_success)) 96 | state.value = false 97 | } 98 | ) 99 | } 100 | DialogButton(true, text = stringResource(id = cancel)) { state.value = false } 101 | } 102 | } 103 | 104 | @Composable 105 | fun ConfigItem( 106 | @DrawableRes id: Int, 107 | text: String, 108 | onClick: () -> Unit = {} 109 | ) { 110 | Row( 111 | modifier = Modifier 112 | .padding(horizontal = 24.dp) 113 | .height(56.dp) 114 | .fillMaxWidth() 115 | .clip(dialogButtonBackground) 116 | .clickable( 117 | onClick = onClick 118 | ) 119 | .padding(horizontal = 16.dp), 120 | verticalAlignment = Alignment.CenterVertically 121 | 122 | ) { 123 | Icon( 124 | painter = painterResource(id = id), 125 | contentDescription = text + "的图标", 126 | tint = colors.secondTextColor 127 | ) 128 | Text( 129 | text = text, 130 | color = colors.secondTextColor, 131 | modifier = Modifier 132 | .padding(horizontal = 16.dp) 133 | .weight(1f) 134 | ) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/Dialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.platform.AbstractComposeView 10 | import androidx.compose.ui.platform.LocalDensity 11 | import androidx.compose.ui.platform.LocalView 12 | import androidx.compose.ui.platform.ViewRootForInspector 13 | import androidx.lifecycle.ViewTreeLifecycleOwner 14 | import androidx.lifecycle.ViewTreeViewModelStoreOwner 15 | import androidx.savedstate.ViewTreeSavedStateRegistryOwner 16 | 17 | class DialogProperties( 18 | val dismissOnBackPress: Boolean = true 19 | ) 20 | 21 | @Composable 22 | fun Dialog( 23 | onRemoveViewRequest: () -> Unit, 24 | onDismissRequest: () -> Unit, 25 | properties: DialogProperties = DialogProperties(), 26 | isDismiss: Boolean, 27 | content: @Composable () -> Unit 28 | ) { 29 | val view = LocalView.current 30 | val density = LocalDensity.current 31 | val composition = rememberCompositionContext() 32 | val currentContent by rememberUpdatedState(content) 33 | val dialog = remember(view, density) { 34 | DialogWrapper( 35 | onDismissRequest, 36 | properties, 37 | view 38 | ).apply { 39 | setContent(composition) { 40 | DialogLayout { 41 | currentContent() 42 | } 43 | } 44 | } 45 | } 46 | if (isDismiss) { 47 | dialog.removeView() 48 | onRemoveViewRequest() 49 | } 50 | 51 | DisposableEffect(dialog) { 52 | dialog.show() 53 | onDispose { 54 | dialog.dismiss() 55 | dialog.disposeComposition() 56 | dialog.removeView() 57 | } 58 | 59 | } 60 | } 61 | 62 | @Suppress("ViewConstructor") 63 | private class DialogDecorLayout( 64 | context: Context, 65 | ) : AbstractComposeView(context) { 66 | 67 | private var content: @Composable () -> Unit by mutableStateOf({}) 68 | 69 | override var shouldCreateCompositionOnAttachedToWindow = false 70 | private set 71 | 72 | fun setContent(parent: CompositionContext, content: @Composable () -> Unit) { 73 | setParentCompositionContext(parent) 74 | this.content = content 75 | shouldCreateCompositionOnAttachedToWindow = true 76 | createComposition() 77 | } 78 | 79 | @Composable 80 | override fun Content() { 81 | content() 82 | } 83 | } 84 | 85 | private class DialogWrapper( 86 | private var onDismissRequest: () -> Unit, 87 | properties: DialogProperties, 88 | composeView: View, 89 | ) : BaseDialog(composeView.context), ViewRootForInspector { 90 | 91 | private val dialogLayout: DialogDecorLayout = DialogDecorLayout(composeView.context) 92 | 93 | override val subCompositionView: AbstractComposeView get() = dialogLayout 94 | 95 | init { 96 | setContentView(dialogLayout) 97 | ViewTreeLifecycleOwner.set(dialogLayout, ViewTreeLifecycleOwner.get(composeView)) 98 | ViewTreeViewModelStoreOwner.set(dialogLayout, ViewTreeViewModelStoreOwner.get(composeView)) 99 | ViewTreeSavedStateRegistryOwner.set( 100 | dialogLayout, 101 | ViewTreeSavedStateRegistryOwner.get(composeView) 102 | ) 103 | setDismissOnBackPress(properties.dismissOnBackPress) 104 | } 105 | 106 | fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) { 107 | dialogLayout.setContent(parentComposition, children) 108 | } 109 | 110 | fun disposeComposition() { 111 | dialogLayout.disposeComposition() 112 | } 113 | 114 | override fun dismiss() { 115 | onDismissRequest() 116 | } 117 | } 118 | 119 | @Composable 120 | private fun DialogLayout( 121 | modifier: Modifier = Modifier, 122 | content: @Composable () -> Unit 123 | ) { 124 | Box(modifier = modifier.fillMaxSize()) { 125 | content() 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/DialogButton.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.material.Text 9 | import androidx.compose.material.ripple.LocalRippleTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import me.kyuubiran.qqcleaner.R 20 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 21 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes 22 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes 23 | import me.kyuubiran.qqcleaner.ui.util.RippleCustomTheme 24 | 25 | @Composable 26 | fun DialogButton( 27 | isFix: Boolean, 28 | text: String = stringResource(id = R.string.confirm), 29 | onClick: () -> Unit 30 | ) { 31 | 32 | CompositionLocalProvider(LocalRippleTheme provides RippleCustomTheme(color = colors.fourPercentThemeColor)) { 33 | val dialogButtonColor by animateColorAsState( 34 | if (!isFix) 35 | colors.twoPercentThemeColor 36 | else 37 | colors.fourPercentThemeColor, 38 | tween(300) 39 | ) 40 | 41 | val dialogButtonTextColor by animateColorAsState( 42 | if (!isFix) 43 | colors.thirtyEightPercentThemeColor 44 | else 45 | colors.mainThemeColor, 46 | tween(300) 47 | ) 48 | Row( 49 | modifier = Modifier 50 | .padding(start = 24.dp, top = 24.dp, end = 24.dp) 51 | .fillMaxWidth() 52 | .height(48.dp) 53 | .background( 54 | color = dialogButtonColor, 55 | shape = QQCleanerShapes.dialogButtonBackground 56 | ) 57 | .clip(shape = QQCleanerShapes.dialogButtonBackground) 58 | .clickable(enabled = isFix, onClick = onClick), 59 | verticalAlignment = Alignment.CenterVertically, 60 | horizontalArrangement = Arrangement.Center 61 | ) { 62 | Text( 63 | modifier = Modifier, 64 | style = QQCleanerTypes.DialogButtonStyle, 65 | color = dialogButtonTextColor, 66 | textAlign = TextAlign.Center, 67 | text = text 68 | ) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/EditDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.app.Activity 4 | import android.graphics.Rect 5 | import android.os.Build.VERSION.SDK_INT 6 | import android.os.Build.VERSION_CODES.R 7 | import android.view.WindowInsets 8 | import android.view.WindowInsetsAnimation 9 | import androidx.compose.animation.core.Animatable 10 | import androidx.compose.animation.core.tween 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.unit.dp 14 | import me.kyuubiran.qqcleaner.ui.util.px2dp 15 | 16 | @Composable 17 | fun EditDialog( 18 | dialogHeight: Float, 19 | dialogText: String, 20 | isSoftShowing: MutableState, 21 | onDismissRequest: () -> Unit, 22 | state: MutableState, 23 | content: @Composable () -> Unit, 24 | 25 | ) { 26 | val context = LocalContext.current as Activity 27 | var softKeyboardHeight by remember { mutableStateOf(0f) } 28 | val bottomHeight = remember { Animatable(0f) } 29 | var hasKeyboard by remember { mutableStateOf(false) } 30 | // 窗口的可见高度 31 | var windowHeight by remember { mutableStateOf(0) } 32 | 33 | fun Int.px2dp(): Float { 34 | return this.px2dp(context) 35 | } 36 | context.window.decorView.apply { 37 | if (SDK_INT >= R) { 38 | val callback = 39 | object : WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) { 40 | // 键盘抬升的时候,键盘的高变化 41 | override fun onProgress( 42 | insets: WindowInsets, 43 | animations: MutableList 44 | ): WindowInsets { 45 | softKeyboardHeight = 46 | insets.getInsets(WindowInsets.Type.ime()).bottom.px2dp() 47 | return insets 48 | } 49 | } 50 | this.setWindowInsetsAnimationCallback(callback) 51 | } else { 52 | this.viewTreeObserver.addOnGlobalLayoutListener { 53 | val r = Rect() 54 | this.getWindowVisibleDisplayFrame(r) 55 | // 这个是当前窗口的可见高度 56 | val visibleHeight = r.height() 57 | if (windowHeight == 0) { 58 | // 等于可见高度 59 | windowHeight = visibleHeight 60 | } else { 61 | softKeyboardHeight = 62 | if (windowHeight == visibleHeight) { 63 | hasKeyboard = false 64 | 0f 65 | } else { 66 | hasKeyboard = true 67 | (windowHeight - visibleHeight).px2dp() 68 | } 69 | } 70 | } 71 | } 72 | } 73 | LaunchedEffect(softKeyboardHeight) { 74 | isSoftShowing.value = softKeyboardHeight > 0 75 | } 76 | // todo 等待分析 77 | if (SDK_INT < R) 78 | LaunchedEffect(hasKeyboard) { 79 | bottomHeight.animateTo( 80 | targetValue = if (hasKeyboard) softKeyboardHeight else 0f, 81 | animationSpec = tween(300) 82 | ) 83 | } 84 | BottomDialog( 85 | onDismissRequest = { 86 | onDismissRequest() 87 | }, 88 | dialogHeight = dialogHeight, 89 | state = state, 90 | // 为啥卡顿我也不清楚,但是我猜想跟这里有关系? 91 | // 需要计算好多次 92 | bottomHeight = if (SDK_INT >= R) 93 | softKeyboardHeight.dp 94 | else 95 | bottomHeight.value.dp, 96 | dialogText = dialogText 97 | ) { 98 | content() 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/FileAddDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.app.Activity 4 | import android.view.KeyEvent 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.text.KeyboardOptions 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.unit.dp 16 | import me.kyuubiran.qqcleaner.ui.composable.EditText 17 | import me.kyuubiran.qqcleaner.ui.util.hideKeyBoard 18 | 19 | @Composable 20 | fun FileAddDialog( 21 | onDismissRequest: () -> Unit 22 | ) { 23 | val state = remember { mutableStateOf(true) } 24 | val isSoftShowing = remember { mutableStateOf(true) } 25 | EditDialog( 26 | onDismissRequest = onDismissRequest, 27 | dialogHeight = 240f, 28 | dialogText = "编辑路径", 29 | isSoftShowing = isSoftShowing, 30 | state = state 31 | ) { 32 | val context = LocalContext.current as Activity 33 | val text = remember { mutableStateOf("") } 34 | val name = remember { mutableStateOf("") } 35 | Column { 36 | 37 | EditText( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .padding(horizontal = 24.dp) 41 | .height(56.dp), 42 | text = name, 43 | keyboardOptions = KeyboardOptions.Default, 44 | onValueChange = { value -> 45 | name.value = value 46 | }, 47 | onKeyEvent = { 48 | // 因为输入的时候焦点会聚集在 输入框,所以输入框的需要进行是否为返回事件的判断 49 | // 实际上还是保留了类似早期 android 及实体按键的东西 50 | // 返回是一个按键 51 | if (this.nativeKeyEvent.action == KeyEvent.ACTION_UP 52 | && this.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_BACK 53 | ) { 54 | if (!isSoftShowing.value) 55 | state.value = false 56 | return@EditText true 57 | } 58 | false 59 | }, 60 | hintText = "配置名称" 61 | ) 62 | 63 | // 判断是否为空,为空的时候无法点击不为空的时候,可以点击 64 | DialogButton(text.value.isNotEmpty()) { 65 | // 需要获取点击之后的内容,text 就可以啦 66 | state.value = false 67 | // 这个是收回输入框 68 | context.hideKeyBoard() 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/FileDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.app.Activity 4 | import android.view.KeyEvent.ACTION_UP 5 | import android.view.KeyEvent.KEYCODE_BACK 6 | import androidx.compose.animation.AnimatedVisibility 7 | import androidx.compose.foundation.Canvas 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.layout.* 11 | import androidx.compose.foundation.text.KeyboardOptions 12 | import androidx.compose.material.Text 13 | import androidx.compose.material.ripple.LocalRippleTheme 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.geometry.Offset 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.dp 22 | import me.kyuubiran.qqcleaner.R.string.dialog_title_time 23 | import me.kyuubiran.qqcleaner.ui.composable.EditText 24 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 25 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes 26 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes.ConfigItemFixStyle 27 | import me.kyuubiran.qqcleaner.ui.util.RippleCustomTheme 28 | import me.kyuubiran.qqcleaner.ui.util.hideKeyBoard 29 | 30 | enum class FileName { 31 | Tencent, 32 | Private, 33 | Public 34 | } 35 | 36 | @Composable 37 | fun FileDialog( 38 | onDismissRequest: (String) -> Unit, 39 | ) { 40 | 41 | val context = LocalContext.current as Activity 42 | val text = remember { mutableStateOf("") } 43 | val state = remember { mutableStateOf(true) } 44 | val isSoftShowing = remember { mutableStateOf(true) } 45 | 46 | var isFile by remember { mutableStateOf(FileName.Public) } 47 | 48 | @Composable 49 | fun FileItem(text: String, file: FileName) { 50 | 51 | CompositionLocalProvider(LocalRippleTheme provides RippleCustomTheme(color = if (isFile == file) colors.whiteColor else colors.fourPercentThemeColor)) { 52 | Row( 53 | modifier = Modifier 54 | .padding(start = 8.dp) 55 | .clip(shape = QQCleanerShapes.dialogConfigItemBackground) 56 | .background( 57 | color = if (isFile == file) colors.mainThemeColor else colors.fourPercentThemeColor 58 | ) 59 | .height(28.dp) 60 | .clickable { 61 | isFile = file 62 | }, 63 | verticalAlignment = Alignment.CenterVertically, 64 | horizontalArrangement = Arrangement.Center 65 | ) { 66 | val color = colors.whiteColor 67 | AnimatedVisibility( 68 | visible = isFile == file, 69 | modifier = Modifier.padding(start = 16.dp, end = 6.dp), 70 | ) { 71 | Canvas( 72 | modifier = Modifier 73 | .size(3.dp) 74 | ) { 75 | val canvasWidth = size.width 76 | val canvasHeight = size.height 77 | drawCircle( 78 | color = color, 79 | center = Offset(x = canvasWidth / 2, y = canvasHeight / 2), 80 | radius = size.minDimension 81 | ) 82 | } 83 | } 84 | 85 | Text( 86 | modifier = Modifier.padding( 87 | start = if (isFile == file) 0.dp else 16.dp, 88 | end = 16.dp 89 | ), 90 | text = text, 91 | style = ConfigItemFixStyle, 92 | color = if (isFile == file) colors.whiteColor else colors.mainThemeColor 93 | ) 94 | } 95 | } 96 | } 97 | 98 | EditDialog( 99 | onDismissRequest = { 100 | onDismissRequest(text.value) 101 | }, 102 | dialogHeight = 308f, 103 | state = state, 104 | isSoftShowing = isSoftShowing, 105 | dialogText = stringResource(id = dialog_title_time) 106 | ) { 107 | EditText( 108 | modifier = Modifier 109 | .fillMaxWidth() 110 | .padding(horizontal = 24.dp) 111 | .height(80.dp), 112 | text = text, 113 | keyboardOptions = KeyboardOptions.Default, 114 | onValueChange = { 115 | }, 116 | onKeyEvent = { 117 | // 因为输入的时候焦点会聚集在 输入框,所以输入框的需要进行是否为返回事件的判断 118 | // 实际上还是保留了类似早期 android 及实体按键的东西 119 | // 返回是一个按键 120 | if (this.nativeKeyEvent.action == ACTION_UP 121 | && this.nativeKeyEvent.keyCode == KEYCODE_BACK 122 | ) { 123 | if (!isSoftShowing.value) 124 | state.value = false 125 | return@EditText true 126 | } 127 | false 128 | }, 129 | // todo 测试内容,所以需要咕咕咕 130 | hintText = "/storage/emulated/0/Android/data/com.tencent.mobileqq/caches" 131 | ) 132 | Row( 133 | modifier = Modifier 134 | .padding(top = 16.dp, bottom = 24.dp) 135 | .padding(horizontal = 24.dp) 136 | .height(28.dp) 137 | .fillMaxWidth() 138 | ) { 139 | FileItem(text = "公开目录", FileName.Public) 140 | FileItem(text = "私有目录", FileName.Private) 141 | FileItem(text = "Tencent 目录", FileName.Tencent) 142 | 143 | 144 | } 145 | // 判断是否为空,为空的时候无法点击不为空的时候,可以点击 146 | DialogButton(text.value.isNotEmpty()) { 147 | // 需要获取点击之后的内容,text 就可以啦 148 | state.value = false 149 | // 这个是收回输入框 150 | context.hideKeyBoard() 151 | } 152 | } 153 | } 154 | 155 | 156 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/SortAddDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.app.Activity 4 | import android.view.KeyEvent 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.text.KeyboardOptions 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import me.kyuubiran.qqcleaner.R 18 | import me.kyuubiran.qqcleaner.ui.composable.EditText 19 | import me.kyuubiran.qqcleaner.ui.util.hideKeyBoard 20 | 21 | @Composable 22 | fun SortAddDialog( 23 | isNew: Boolean = false, 24 | onDismissRequest: () -> Unit = {}, 25 | ) { 26 | val state = remember { mutableStateOf(true) } 27 | val isSoftShowing = remember { mutableStateOf(true) } 28 | EditDialog( 29 | onDismissRequest = onDismissRequest, 30 | dialogHeight = 240f, 31 | dialogText = stringResource(id = if (isNew) R.string.dialog_title_new_path else R.string.dialog_title_change_path_name), 32 | isSoftShowing = isSoftShowing, 33 | state = state 34 | ) { 35 | val context = LocalContext.current as Activity 36 | val text = remember { mutableStateOf("") } 37 | val name = remember { mutableStateOf("") } 38 | Column { 39 | 40 | EditText( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .padding(horizontal = 24.dp) 44 | .height(56.dp), 45 | text = name, 46 | keyboardOptions = KeyboardOptions.Default, 47 | onValueChange = { value -> 48 | name.value = value 49 | }, 50 | onKeyEvent = { 51 | // 因为输入的时候焦点会聚集在 输入框,所以输入框的需要进行是否为返回事件的判断 52 | // 实际上还是保留了类似早期 android 及实体按键的东西 53 | // 返回是一个按键 54 | if (this.nativeKeyEvent.action == KeyEvent.ACTION_UP 55 | && this.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_BACK 56 | ) { 57 | if (!isSoftShowing.value) 58 | state.value = false 59 | return@EditText true 60 | } 61 | false 62 | }, 63 | hintText = stringResource(id = R.string.path_name) 64 | ) 65 | 66 | // 判断是否为空,为空的时候无法点击不为空的时候,可以点击 67 | DialogButton(text.value.isNotEmpty()) { 68 | // 需要获取点击之后的内容,text 就可以啦 69 | state.value = false 70 | // 这个是收回输入框 71 | context.hideKeyBoard() 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/ThemeDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material.Icon 11 | import androidx.compose.material.Text 12 | import androidx.compose.material.ripple.LocalRippleTheme 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import me.kyuubiran.qqcleaner.QQCleanerData.isBlack 22 | import me.kyuubiran.qqcleaner.QQCleanerData.theme 23 | import me.kyuubiran.qqcleaner.R 24 | import me.kyuubiran.qqcleaner.ui.composable.Line 25 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.Theme.* 26 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 27 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes.dialogButtonBackground 28 | import me.kyuubiran.qqcleaner.ui.util.RippleCustomTheme 29 | import me.kyuubiran.qqcleaner.util.ConfigManager 30 | 31 | @Composable 32 | fun ThemeDialog( 33 | onDismissRequest: () -> Unit 34 | ) { 35 | 36 | // 这里是当前主题的获取 37 | var currentTheme by remember { 38 | mutableStateOf( 39 | // 只在第一次调用 40 | when (theme) { 41 | Light -> 0x0 42 | Dark -> 0x1 43 | System -> 0x2 44 | } 45 | ) 46 | } 47 | var currentBlack by remember { mutableStateOf(isBlack) } 48 | // 用 state 来进行关闭弹窗是哪个人想出来的主意,真烂,哦,是我自己,那没事了 49 | val state = remember { mutableStateOf(true) } 50 | // 这是用 BottomDialog 组合出来的弹窗,BottomDialog 还有一部分功能没加 51 | BottomDialog( 52 | onDismissRequest = onDismissRequest, 53 | state = state, 54 | dialogHeight = 432f, 55 | dialogText = stringResource(id = R.string.item_theme) 56 | ) { 57 | // 线条绘制 58 | Line( 59 | Modifier 60 | .padding(top = 4.dp, start = 32.dp, end = 32.dp, bottom = 12.dp) 61 | .fillMaxWidth() 62 | .height(1.dp) 63 | ) 64 | // 下面是对应的主题 65 | ThemeItem( 66 | text = stringResource(id = R.string.light_theme), 67 | id = R.drawable.ic_sun, 68 | checked = currentTheme == 0x0, 69 | onClick = { 70 | currentTheme = 0x0 71 | } 72 | ) 73 | ThemeItem( 74 | text = stringResource(id = R.string.dark_theme), 75 | id = R.drawable.ic_moon, 76 | checked = currentTheme == 0x1, 77 | onClick = { 78 | currentTheme = 0x1 79 | } 80 | ) 81 | ThemeItem( 82 | text = stringResource(id = R.string.follow_system_theme), 83 | id = R.drawable.ic_android, 84 | checked = currentTheme == 0x2, 85 | onClick = { 86 | currentTheme = 0x2 87 | } 88 | ) 89 | 90 | // 线条绘制 91 | Line( 92 | modifier = Modifier 93 | .padding(top = 12.dp, start = 32.dp, end = 32.dp, bottom = 12.dp) 94 | .fillMaxWidth() 95 | .height(1.dp) 96 | ) 97 | 98 | ThemeItem( 99 | text = stringResource(id = R.string.use_black_dark_theme), 100 | id = R.drawable.ic_a, 101 | checked = currentBlack, 102 | onClick = { 103 | currentBlack = !currentBlack 104 | } 105 | 106 | ) 107 | 108 | DialogButton( 109 | // 这里进行当前选择和主题是否相同,如果不同则则可以点击选择 110 | (currentTheme != when (theme) { 111 | Light -> 0x0 112 | Dark -> 0x1 113 | System -> 0x2 114 | }) || (currentBlack != isBlack) 115 | ) { 116 | when (currentTheme) { 117 | 0x0 -> theme = Light 118 | 0x1 -> theme = Dark 119 | 0x2 -> theme = System 120 | 121 | } 122 | isBlack = currentBlack 123 | ConfigManager.sThemeSelect = currentTheme 124 | state.value = false 125 | } 126 | } 127 | } 128 | 129 | @Composable 130 | private fun ThemeItem( 131 | @DrawableRes id: Int, 132 | text: String, 133 | checked: Boolean = false, 134 | onClick: () -> Unit = {} 135 | ) { 136 | // 如果添加 animateColorAsState 动画,则在点击确定的时候有概率闪退,我也不知道什么问题,不过这个加不加点击动画都那样不管他 137 | val backgroundColor = if (checked) colors.fourPercentThemeColor else Color.Transparent 138 | val textColor = if (checked) colors.mainThemeColor else colors.secondTextColor 139 | // 替换了水波纹原本的颜色 140 | CompositionLocalProvider(LocalRippleTheme provides RippleCustomTheme(color = if (checked) colors.fourPercentThemeColor else colors.rippleColor)) { 141 | Row( 142 | modifier = Modifier 143 | .padding(horizontal = 24.dp) 144 | .height(56.dp) 145 | .fillMaxWidth() 146 | .clip(dialogButtonBackground) 147 | .background(color = backgroundColor) 148 | .clickable( 149 | onClick = onClick 150 | ) 151 | .padding(horizontal = 16.dp), 152 | verticalAlignment = Alignment.CenterVertically 153 | ) { 154 | Icon( 155 | painter = painterResource(id = id), 156 | contentDescription = text + "的图标", 157 | tint = textColor 158 | ) 159 | Text( 160 | text = text, 161 | color = textColor, 162 | modifier = Modifier 163 | .padding(horizontal = 16.dp) 164 | .weight(1f) 165 | ) 166 | if (checked) { 167 | Icon( 168 | painter = painterResource(id = R.drawable.ic_chosen), 169 | contentDescription = "选择图标", 170 | tint = textColor 171 | ) 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/TimeDialog.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog 2 | 3 | import android.app.Activity 4 | import android.view.KeyEvent.ACTION_UP 5 | import android.view.KeyEvent.KEYCODE_BACK 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.text.KeyboardOptions 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.text.input.KeyboardType 17 | import androidx.compose.ui.unit.dp 18 | import me.kyuubiran.qqcleaner.R 19 | import me.kyuubiran.qqcleaner.R.string.dialog_title_time 20 | import me.kyuubiran.qqcleaner.ui.composable.EditText 21 | import me.kyuubiran.qqcleaner.ui.util.hideKeyBoard 22 | 23 | @Composable 24 | fun TimeDialog( 25 | onDismissRequest: (String) -> Unit, 26 | ) { 27 | 28 | val context = LocalContext.current as Activity 29 | val text = remember { mutableStateOf("") } 30 | val state = remember { mutableStateOf(true) } 31 | 32 | val isSoftShowing = remember { mutableStateOf(true) } 33 | EditDialog( 34 | onDismissRequest = { 35 | onDismissRequest(text.value) 36 | }, 37 | dialogHeight = 240f, 38 | state = state, 39 | isSoftShowing = isSoftShowing, 40 | dialogText = stringResource(id = dialog_title_time) 41 | ) { 42 | EditText( 43 | modifier = Modifier 44 | .fillMaxWidth() 45 | .padding(horizontal = 24.dp) 46 | .height(56.dp), 47 | text = text, 48 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), 49 | onValueChange = { value -> 50 | text.value = value.filter { 51 | it.isDigit() 52 | } 53 | }, 54 | onKeyEvent = { 55 | // 因为输入的时候焦点会聚集在 输入框,所以输入框的需要进行是否为返回事件的判断 56 | // 实际上还是保留了类似早期 android 及实体按键的东西 57 | // 返回是一个按键 58 | if (this.nativeKeyEvent.action == ACTION_UP 59 | && this.nativeKeyEvent.keyCode == KEYCODE_BACK 60 | ) { 61 | if (!isSoftShowing.value) 62 | state.value = false 63 | return@EditText true 64 | } 65 | false 66 | }, 67 | hintText = stringResource(id = R.string.set_auto_clean_interval_desc) 68 | ) 69 | // 判断是否为空,为空的时候无法点击不为空的时候,可以点击 70 | DialogButton(text.value.isNotEmpty()) { 71 | // 需要获取点击之后的内容,text 就可以啦 72 | state.value = false 73 | // 这个是收回输入框 74 | context.hideKeyBoard() 75 | } 76 | } 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/composable/dialog/view/DialogBaseView.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.composable.dialog.view 2 | 3 | import android.content.Context 4 | import android.view.KeyEvent 5 | import android.view.KeyEvent.ACTION_UP 6 | import android.view.KeyEvent.KEYCODE_BACK 7 | import android.view.View 8 | 9 | /** 10 | * 获取按键事件 11 | * @author Agoines 12 | * @version 1.0 13 | */ 14 | class DialogBaseView(context: Context) : View(context) { 15 | 16 | /** 17 | * 按键事件监听器 18 | */ 19 | private lateinit var onBackPressed: () -> Unit 20 | 21 | /** 22 | * DialogBaseRelativeLayout 获取焦点 23 | */ 24 | fun setFocusable() { 25 | isFocusable = true 26 | isFocusableInTouchMode = true 27 | requestFocus() 28 | } 29 | 30 | /** 31 | * 劫持对应的点击事件,并完成对应效果 32 | */ 33 | override fun dispatchKeyEvent(event: KeyEvent): Boolean { 34 | // 返回事件 35 | if (isAttachedToWindow && event.action == ACTION_UP && event.keyCode == KEYCODE_BACK) { 36 | onBackPressed.invoke() 37 | return true 38 | } 39 | return false 40 | } 41 | 42 | /** 43 | * 设置按键事件监听器 44 | */ 45 | fun setOnBackPressed(onBackPressed: () -> Unit) { 46 | this.onBackPressed = onBackPressed 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/scene/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.scene 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.compose.animation.Crossfade 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.Image 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import androidx.navigation.NavController 18 | import me.kyuubiran.qqcleaner.BuildConfig 19 | import me.kyuubiran.qqcleaner.QQCleanerData 20 | import me.kyuubiran.qqcleaner.R 21 | import me.kyuubiran.qqcleaner.ui.QQCleanerApp 22 | import me.kyuubiran.qqcleaner.ui.composable.Item 23 | import me.kyuubiran.qqcleaner.ui.composable.TopBar 24 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 25 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes.AboutTextStyle 26 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes.VersionTextStyle 27 | 28 | @Composable 29 | fun AboutScreen(navController: NavController) { 30 | Column( 31 | modifier = Modifier 32 | .fillMaxSize() 33 | .background(color = colors.pageBackgroundColor) 34 | .statusBarsPadding() 35 | ) { 36 | TopBar( 37 | click = { 38 | navController.popBackStack(navController.graph.startDestinationId, false) 39 | }, 40 | stringResource(id = R.string.title_about) 41 | ) 42 | Crossfade( 43 | targetState = QQCleanerData.isDark, 44 | animationSpec = tween(600), 45 | modifier = Modifier.fillMaxWidth() 46 | ) { 47 | Column( 48 | modifier = Modifier 49 | .padding(top = 24.dp) 50 | .fillMaxWidth() 51 | ) { 52 | Image( 53 | modifier = Modifier 54 | .align(Alignment.CenterHorizontally) 55 | .size(96.dp), 56 | painter = painterResource( 57 | id = 58 | if (it) 59 | R.drawable.ic_home_qqcleaner_dark 60 | else 61 | R.drawable.ic_home_qqcleaner 62 | ), 63 | contentDescription = stringResource(id = R.string.icon_content_description), 64 | ) 65 | 66 | Text( 67 | text = stringResource(id = R.string.module_name), 68 | modifier = Modifier 69 | .padding(top = 24.dp) 70 | .align(Alignment.CenterHorizontally), 71 | style = AboutTextStyle, 72 | color = colors.firstTextColor 73 | ) 74 | Text( 75 | text = "${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})", 76 | modifier = Modifier 77 | .padding(top = 18.dp) 78 | .align(Alignment.CenterHorizontally), 79 | style = VersionTextStyle, 80 | color = colors.secondTextColor 81 | ) 82 | Spacer( 83 | Modifier 84 | .fillMaxWidth() 85 | .height(24.dp) 86 | ) 87 | CardGroup(56.dp) { 88 | Item( 89 | text = stringResource(id = R.string.goto_github), 90 | onClick = { 91 | navController.context.startActivity(Intent().apply { 92 | action = Intent.ACTION_VIEW 93 | data = Uri.parse("https://github.com/KitsunePie/QQCleaner") 94 | } 95 | ) 96 | }, 97 | content = { 98 | ForwardIcon(R.string.item_about) 99 | } 100 | ) 101 | } 102 | 103 | Spacer( 104 | Modifier 105 | .fillMaxWidth() 106 | .height(24.dp) 107 | ) 108 | CardGroup(112.dp) { 109 | Item( 110 | text = stringResource(id = R.string.join_tg_channel), 111 | onClick = { 112 | navController.context.startActivity(Intent().apply { 113 | action = Intent.ACTION_VIEW 114 | data = Uri.parse("https://t.me/QQCleaner") 115 | }) 116 | }, 117 | content = { 118 | ForwardIcon(R.string.item_about) 119 | } 120 | ) 121 | Item( 122 | text = stringResource(id = R.string.join_tg_group), 123 | onClick = { 124 | navController.context.startActivity(Intent().apply { 125 | action = Intent.ACTION_VIEW 126 | data = Uri.parse("https://t.me/QQCleanerChat") 127 | }) 128 | }, 129 | content = { 130 | ForwardIcon(R.string.item_about) 131 | } 132 | ) 133 | } 134 | 135 | Spacer( 136 | Modifier 137 | .fillMaxWidth() 138 | .height(24.dp) 139 | ) 140 | CardGroup(56.dp) { 141 | Item( 142 | text = stringResource(id = R.string.title_dev), 143 | onClick = { 144 | navController.navigate(QQCleanerApp.Developer) 145 | }, 146 | content = { 147 | ForwardIcon(R.string.item_about) 148 | } 149 | ) 150 | } 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/scene/DeveloperScreen.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.scene 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import androidx.navigation.NavController 19 | import me.kyuubiran.qqcleaner.R 20 | import me.kyuubiran.qqcleaner.ui.composable.TopBar 21 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 22 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes 23 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes.DescribeTextStyle 24 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes.NameTextStyle 25 | 26 | @Composable 27 | fun DeveloperScreen(navController: NavController) { 28 | Column( 29 | modifier = Modifier 30 | .fillMaxSize() 31 | .background(color = colors.pageBackgroundColor) 32 | .statusBarsPadding() 33 | ) { 34 | TopBar( 35 | click = { 36 | navController.popBackStack() 37 | }, 38 | stringResource(id = R.string.title_dev) 39 | ) 40 | Spacer( 41 | Modifier 42 | .fillMaxWidth() 43 | .height(24.dp) 44 | ) 45 | CardGroup(72.dp) { 46 | DevItem( 47 | name = stringResource(id = R.string.dev_org_name), 48 | text = stringResource(id = R.string.dev_org_desc), 49 | id = R.drawable.ic_developer_kitsunepie, 50 | uri = "https://github.com/KitsunePie" 51 | ) 52 | } 53 | CardTitle(text = stringResource(id = R.string.title_dev)) 54 | CardGroup(360.dp) { 55 | DevItem( 56 | name = stringResource(id = R.string.dev_KyuubiRan_name), 57 | text = stringResource(id = R.string.dev_KyuubiRan_desc), 58 | id = R.drawable.ic_developer_kyuubiran, 59 | uri = "https://github.com/KyuubiRan" 60 | ) 61 | DevItem( 62 | name = stringResource(id = R.string.dev_Ketal_name), 63 | text = stringResource(id = R.string.dev_Ketal_desc), 64 | id = R.drawable.ic_developer_ketal, 65 | uri = "https://github.com/keta1" 66 | ) 67 | DevItem( 68 | name = stringResource(id = R.string.dev_NextAlone_name), 69 | text = stringResource(id = R.string.dev_NextAlone_desc), 70 | id = R.drawable.ic_developer_nextalone, 71 | uri = "https://github.com/NextAlone" 72 | ) 73 | DevItem( 74 | name = stringResource(id = R.string.dev_Agoines_name), 75 | text = stringResource(id = R.string.dev_Agoines_desc), 76 | id = R.drawable.ic_developer_agoines, 77 | uri = "https://github.com/Agoines" 78 | ) 79 | DevItem( 80 | name = stringResource(id = R.string.dev_MaiTungTM_name), 81 | text = stringResource(id = R.string.dev_MaiTungTM_desc), 82 | id = R.drawable.ic_developer_maitungtm, 83 | uri = "https://github.com/Lagrio" 84 | ) 85 | } 86 | } 87 | } 88 | 89 | @Composable 90 | fun DevItem( 91 | name: String, 92 | text: String, 93 | id: Int, 94 | uri: String 95 | ) { 96 | val context = LocalContext.current 97 | Row( 98 | Modifier 99 | .height(72.dp) 100 | .fillMaxWidth() 101 | .clip(QQCleanerShapes.cardGroupBackground) 102 | .clickable { 103 | context.startActivity(Intent().apply { 104 | action = Intent.ACTION_VIEW 105 | data = Uri.parse(uri) 106 | } 107 | ) 108 | } 109 | .padding(horizontal = 16.dp), 110 | horizontalArrangement = Arrangement.Center, 111 | verticalAlignment = Alignment.CenterVertically, 112 | ) { 113 | Image( 114 | modifier = Modifier 115 | .padding(end = 16.dp) 116 | // 这个命名当时没取好,迟点改改 117 | .clip(QQCleanerShapes.cardGroupBackground) 118 | .size(40.dp), 119 | painter = painterResource(id = id), 120 | contentDescription = "$text + $name 的头像" 121 | ) 122 | Column(modifier = Modifier.weight(1f)) { 123 | Text(text = name, style = NameTextStyle, color = colors.secondTextColor) 124 | Text(text = text, style = DescribeTextStyle, color = colors.thirdTextColor) 125 | } 126 | ForwardIcon(id = R.string.item_about) 127 | } 128 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/scene/SortFixScreen.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.scene 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.material.Icon 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.res.painterResource 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.text.style.TextAlign 17 | import androidx.compose.ui.unit.dp 18 | import androidx.navigation.NavController 19 | import me.kyuubiran.qqcleaner.QQCleanerData 20 | import me.kyuubiran.qqcleaner.R 21 | import me.kyuubiran.qqcleaner.ui.composable.Fab 22 | import me.kyuubiran.qqcleaner.ui.composable.TopBar 23 | import me.kyuubiran.qqcleaner.ui.composable.dialog.FileAddDialog 24 | import me.kyuubiran.qqcleaner.ui.composable.dialog.FileDialog 25 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerColorTheme.colors 26 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerShapes 27 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes 28 | import me.kyuubiran.qqcleaner.ui.theme.QQCleanerTypes.FileTextStyle 29 | import me.kyuubiran.qqcleaner.ui.util.Shared 30 | 31 | /** 32 | * 这里是修改类别的 UI 33 | */ 34 | @Composable 35 | fun SortFixScreen(navController: NavController) { 36 | var fileDialogShow by remember { mutableStateOf(false) } 37 | if (fileDialogShow) { 38 | FileDialog { 39 | fileDialogShow = false 40 | } 41 | } 42 | 43 | var fileAddDialogShow by remember { mutableStateOf(false) } 44 | if (fileAddDialogShow) { 45 | FileAddDialog { 46 | fileDialogShow = false 47 | } 48 | } 49 | 50 | Box( 51 | modifier = Modifier 52 | .fillMaxSize() 53 | .background(color = colors.pageBackgroundColor) 54 | .statusBarsPadding() 55 | ) { 56 | Column { 57 | TopBar( 58 | backClick = { 59 | navController.popBackStack() 60 | }, 61 | iconClick = { 62 | 63 | }, 64 | titleText = Shared.currentEditCleanData.title, 65 | id = R.drawable.ic_save 66 | ) 67 | 68 | if (Shared.currentEditCleanPathData.pathList.isNotEmpty()) { 69 | LazyColumn( 70 | modifier = Modifier 71 | .padding(top = 24.dp) 72 | .padding(horizontal = 24.dp) 73 | .background( 74 | color = colors.appBarsAndItemBackgroundColor, 75 | shape = QQCleanerShapes.cardGroupBackground 76 | ) 77 | ) { 78 | items(Shared.currentEditCleanPathData.pathList.size) { item -> 79 | FileItem( 80 | text = "这个是第${item}个,啦啦啦啦啦啦", 81 | onClick = { 82 | fileDialogShow = true 83 | } 84 | ) 85 | } 86 | } 87 | } else { 88 | Column( 89 | Modifier.fillMaxSize(), 90 | horizontalAlignment = Alignment.CenterHorizontally, 91 | verticalArrangement = Arrangement.Center 92 | ) { 93 | Image( 94 | painter = 95 | if (QQCleanerData.isDark) 96 | painterResource(id = R.drawable.ic_list_empty_dark) 97 | else 98 | painterResource(id = R.drawable.ic_list_empty), 99 | contentDescription = stringResource( 100 | id = R.string.list_empty 101 | ), modifier = Modifier.size(96.dp) 102 | ) 103 | Text( 104 | text = stringResource(R.string.sort_empty_tip), 105 | style = QQCleanerTypes.EmptyTipStyle, 106 | color = colors.thirdTextColor, 107 | textAlign = TextAlign.Center, 108 | modifier = Modifier.padding(top = 24.dp) 109 | ) 110 | } 111 | } 112 | } 113 | Fab( 114 | modifier = Modifier 115 | .align(Alignment.BottomCenter), 116 | text = stringResource(R.string.sort_fab_text), 117 | onClick = { 118 | fileAddDialogShow = true 119 | } 120 | ) 121 | } 122 | } 123 | 124 | @Composable 125 | fun FileItem( 126 | onClick: () -> Unit, 127 | text: String 128 | ) { 129 | Row( 130 | modifier = Modifier 131 | .fillMaxWidth() 132 | .defaultMinSize(minHeight = 56.dp) 133 | .clip(shape = QQCleanerShapes.cardGroupBackground) 134 | .clickable { onClick() } 135 | .padding(horizontal = 16.dp), 136 | verticalAlignment = Alignment.CenterVertically, 137 | ) { 138 | Icon( 139 | painter = painterResource(id = R.drawable.ic_file), 140 | tint = colors.secondTextColor, 141 | contentDescription = stringResource(R.string.sort_icon_tip) 142 | ) 143 | Text( 144 | text = text, 145 | style = FileTextStyle, 146 | color = colors.secondTextColor, 147 | modifier = Modifier.padding(start = 16.dp) 148 | ) 149 | 150 | } 151 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/theme/QQCleanerShapes.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.ui.unit.dp 5 | 6 | 7 | object QQCleanerShapes { 8 | val cardBackground = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp) 9 | val cardGroupBackground = RoundedCornerShape(10.dp) 10 | val dialogEditBackGround = RoundedCornerShape(10.dp) 11 | val dialogButtonBackground = RoundedCornerShape(10.dp) 12 | val dialogConfigItemBackground = RoundedCornerShape(16.dp) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/theme/QQCleanerTypes.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.theme 2 | 3 | import androidx.compose.ui.text.TextStyle 4 | import androidx.compose.ui.text.font.FontFamily.Companion.Default 5 | import androidx.compose.ui.text.font.FontWeight 6 | import androidx.compose.ui.text.font.FontWeight.Companion.Bold 7 | import androidx.compose.ui.text.font.FontWeight.Companion.Normal 8 | import androidx.compose.ui.text.font.SystemFontFamily 9 | import androidx.compose.ui.text.style.TextAlign 10 | import androidx.compose.ui.text.style.TextAlign.Companion.Start 11 | import androidx.compose.ui.unit.TextUnit 12 | import androidx.compose.ui.unit.sp 13 | 14 | object QQCleanerTypes { 15 | val cleanerTextStyle = intoTextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = Normal) 16 | val cardTitleTextStyle = intoTextStyle(fontSize = 18.sp, lineHeight = 26.sp, fontWeight = Bold) 17 | val itemTextStyle = intoTextStyle(fontSize = 16.sp, lineHeight = 16.sp, fontWeight = Normal) 18 | val TitleTextStyle = intoTextStyle(fontSize = 24.sp, lineHeight = 26.sp, fontWeight = Normal) 19 | val SubTitleTextStyle = intoTextStyle(fontSize = 18.sp, lineHeight = 26.sp, fontWeight = Bold) 20 | val ButtonTitleTextStyle = 21 | intoTextStyle(fontSize = 8.sp, lineHeight = 26.sp, fontWeight = Normal) 22 | val TitleStyle = intoTextStyle(fontSize = 20.sp, lineHeight = 24.sp, fontWeight = Bold) 23 | val DialogTitleStyle = intoTextStyle(fontSize = 18.sp, lineHeight = 24.sp, fontWeight = Bold) 24 | val DialogEditStyle = intoTextStyle( 25 | fontSize = 16.sp, 26 | lineHeight = 24.sp, 27 | fontWeight = Normal 28 | ) 29 | val ConfigItemFixStyle = 30 | intoTextStyle(fontSize = 10.sp, lineHeight = 16.sp, fontWeight = Normal) 31 | val DialogButtonStyle = intoTextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = Normal) 32 | val TipStyle = intoTextStyle(fontSize = 12.sp, lineHeight = 12.sp, fontWeight = Normal) 33 | val EmptyTipStyle = intoTextStyle(fontSize = 16.sp, lineHeight = 19.sp, fontWeight = Normal) 34 | 35 | val FileTextStyle = intoTextStyle(fontSize = 12.sp, lineHeight = 19.sp, fontWeight = Normal) 36 | 37 | val AboutTextStyle = intoTextStyle(fontSize = 18.sp, lineHeight = 24.sp, fontWeight = Bold) 38 | val VersionTextStyle = intoTextStyle(fontSize = 12.sp, lineHeight = 24.sp, fontWeight = Normal) 39 | val NameTextStyle = intoTextStyle(fontSize = 18.sp, lineHeight = 24.sp, fontWeight = Normal) 40 | val DescribeTextStyle = intoTextStyle(fontSize = 12.sp, lineHeight = 19.sp, fontWeight = Normal) 41 | } 42 | 43 | private fun intoTextStyle( 44 | fontFamily: SystemFontFamily = Default, 45 | fontWeight: FontWeight? = Normal, 46 | fontSize: TextUnit, 47 | textAlign: TextAlign = Start, 48 | lineHeight: TextUnit 49 | ) = TextStyle( 50 | fontFamily = fontFamily, 51 | fontWeight = fontWeight, 52 | fontSize = fontSize, 53 | textAlign = textAlign, 54 | lineHeight = lineHeight, 55 | ) 56 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/util/ColorUtils.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.util 2 | 3 | object ColorUtils { 4 | 5 | /** 6 | * 进行颜色混合的 api 7 | * @param color1 颜色1 8 | * @param color1 颜色2 9 | * @param ratio 混合比例(两个颜色总和为1) 10 | * @return 返回对应的颜色 11 | */ 12 | fun mixColor(color1: Int, color2: Int, ratio: Float): Int { 13 | val inverse = 1 - ratio 14 | val a = (color1 ushr 24) * inverse + (color2 ushr 24) * ratio 15 | val r = (color1 shr 16 and 0xFF) * inverse + (color2 shr 16 and 0xFF) * ratio 16 | val g = (color1 shr 8 and 0xFF) * inverse + (color2 shr 8 and 0xFF) * ratio 17 | val b = (color1 and 0xFF) * inverse + (color2 and 0xFF) * ratio 18 | return a.toInt() shl 24 or (r.toInt() shl 16) or (g.toInt() shl 8) or b.toInt() 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/util/Dp.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.util 2 | 3 | import android.content.Context 4 | 5 | fun Float.dp2px(context: Context): Float { 6 | return 0.5f + this * context.resources.displayMetrics.density 7 | } 8 | 9 | fun Int.dp2px(context: Context): Float { 10 | return 0.5f + this * context.resources.displayMetrics.density 11 | } 12 | 13 | fun Float.px2dp(context: Context): Float { 14 | return this / context.resources.displayMetrics.density 15 | } 16 | 17 | fun Int.px2dp(context: Context): Float { 18 | return this / context.resources.displayMetrics.density 19 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/util/KeyboardUtils.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.util 2 | 3 | import android.app.Activity 4 | import android.content.Context.INPUT_METHOD_SERVICE 5 | import android.view.inputmethod.InputMethodManager 6 | 7 | /** 8 | * 回收键盘 9 | */ 10 | fun Activity.hideKeyBoard() { 11 | val imm = 12 | this.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager? 13 | imm?.hideSoftInputFromWindow(this.window.decorView.windowToken, 0) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/util/Shadow.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.util 2 | 3 | 4 | import android.graphics.Color.toArgb 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.draw.drawBehind 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.graphics.Paint 9 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.unit.dp 12 | 13 | fun Modifier.drawColoredShadow( 14 | color: Color, 15 | alpha: Float = 0.2f, 16 | borderRadius: Dp = 0.dp, 17 | shadowRadius: Dp = 0.dp, 18 | offsetX: Dp = 0.dp, 19 | offsetY: Dp = 0.dp, 20 | roundedRect: Boolean = true 21 | ) = this.drawBehind { 22 | /**将颜色转换为Argb的Int类型*/ 23 | val transparentColor = toArgb(color.copy(alpha = .0f).value.toLong()) 24 | val shadowColor = toArgb(color.copy(alpha = alpha).value.toLong()) 25 | /**调用Canvas绘制*/ 26 | this.drawIntoCanvas { 27 | val paint = Paint() 28 | paint.color = Color.Transparent 29 | /**调用底层fragment Paint绘制*/ 30 | val frameworkPaint = paint.asFrameworkPaint() 31 | frameworkPaint.color = transparentColor 32 | /**绘制阴影*/ 33 | frameworkPaint.setShadowLayer( 34 | shadowRadius.toPx(), 35 | offsetX.toPx(), 36 | offsetY.toPx(), 37 | shadowColor 38 | ) 39 | /**形状绘制*/ 40 | it.drawRoundRect( 41 | 0f, 42 | 0f, 43 | this.size.width, 44 | this.size.height, 45 | if (roundedRect) this.size.height / 2 else borderRadius.toPx(), 46 | if (roundedRect) this.size.height / 2 else borderRadius.toPx(), 47 | paint 48 | ) 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/util/Shared.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.util 2 | 3 | import me.kyuubiran.qqcleaner.data.CleanData 4 | 5 | object Shared { 6 | lateinit var currentEditCleanData: CleanData 7 | lateinit var currentEditCleanPathData: CleanData.PathData 8 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/ui/util/clickable.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.ui.util 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.material.ripple.RippleAlpha 6 | import androidx.compose.material.ripple.RippleTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.composed 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.toArgb 13 | import me.kyuubiran.qqcleaner.QQCleanerData 14 | 15 | /** 16 | * 就是为了少写几行代码 17 | * 其实用的地方也不多 18 | * 就是去掉点击和点击水波纹 19 | * 如果有其他 View 的时候,他可以保证焦点还在 Compose 上 20 | */ 21 | fun Modifier.noClick(): Modifier = composed { 22 | this.clickable( 23 | // 防止击穿 24 | onClick = {}, 25 | // 去掉点击水波纹 26 | indication = null, 27 | interactionSource = remember { MutableInteractionSource() } 28 | ) 29 | } 30 | 31 | // 恕我直言,这个水波纹真的不优雅,我想给这xx两巴掌 32 | /** 33 | * 这是一个水波纹 34 | * @param color 水波纹颜色 35 | */ 36 | class RippleCustomTheme(private val color: Color) : RippleTheme { 37 | @Composable 38 | override fun defaultColor() = 39 | RippleTheme.defaultRippleColor( 40 | Color(color = color.toArgb()), 41 | lightTheme = !QQCleanerData.isDark 42 | ) 43 | 44 | @Composable 45 | override fun rippleAlpha(): RippleAlpha = 46 | RippleTheme.defaultRippleAlpha( 47 | Color(color = color.toArgb()), 48 | lightTheme = !QQCleanerData.isDark 49 | ) 50 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/AutoCleanManager.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import com.github.kyuubiran.ezxhelper.utils.Log 4 | import com.github.kyuubiran.ezxhelper.utils.mainHandler 5 | 6 | object AutoCleanManager : Runnable { 7 | val initAutoClean by lazy { 8 | Log.i("Init auto clean") 9 | mainHandler.postDelayed(this, 1000 * 30) 10 | } 11 | 12 | private fun needClean(): Boolean { 13 | return System.currentTimeMillis() - ConfigManager.sLastCleanDate > (ConfigManager.sAutoCleanInterval * 60L * 60L * 1000L) 14 | } 15 | 16 | override fun run() { 17 | if (ConfigManager.sAutoClean && needClean() && !CleanManager.isConfigEmpty()) { 18 | ConfigManager.sLastCleanDate = System.currentTimeMillis() 19 | CleanManager.executeAll(!ConfigManager.sSilenceClean) 20 | } 21 | mainHandler.postDelayed(this, 1000 * 60 * 10) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/CleanManager.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import com.github.kyuubiran.ezxhelper.init.InitFields.moduleRes 4 | import com.github.kyuubiran.ezxhelper.utils.Log 5 | import com.github.kyuubiran.ezxhelper.utils.Log.logeIfThrow 6 | import me.kyuubiran.qqcleaner.R 7 | import me.kyuubiran.qqcleaner.data.CleanData 8 | import me.kyuubiran.qqcleaner.util.path.CommonPath 9 | import java.io.File 10 | import java.util.concurrent.LinkedBlockingQueue 11 | import java.util.concurrent.ThreadPoolExecutor 12 | import java.util.concurrent.TimeUnit 13 | import kotlin.concurrent.thread 14 | 15 | object CleanManager { 16 | private val pool = ThreadPoolExecutor(1, 1, 5L, TimeUnit.MINUTES, LinkedBlockingQueue(256)) 17 | 18 | fun execute(data: CleanData, showToast: Boolean = true, forceExec: Boolean = false) { 19 | if (!data.valid) return 20 | if (!data.enable && !forceExec) return 21 | pool.execute e@{ 22 | if (showToast) Log.toast( 23 | moduleRes.getString(R.string.executing_config).format(data.title) 24 | ) 25 | runCatching { 26 | data.content.forEach { data -> 27 | if (!data.enable) return@forEach 28 | data.pathList.forEach { path -> 29 | deleteAll(PathUtil.getFullPath(path)) 30 | } 31 | } 32 | }.logeIfThrow("Execute failed, skipped: ${data.title}") { 33 | if (showToast) Log.toast( 34 | moduleRes.getString(R.string.clean_failed).format(data.title) 35 | ) 36 | } 37 | } 38 | } 39 | 40 | fun executeAll(showToast: Boolean = true) { 41 | if (showToast) Log.toast(moduleRes.getString(R.string.clean_start)) 42 | getAllConfigsAsync { 43 | if (it.isEmpty() || it.all { c -> !c.enable }) { 44 | if (showToast) Log.toast(moduleRes.getString(R.string.no_config_enabled)) 45 | } else { 46 | it.forEach { data -> 47 | execute(data, showToast) 48 | } 49 | pool.execute { 50 | if (showToast) Log.toast(moduleRes.getString(R.string.clean_finished)) 51 | } 52 | } 53 | } 54 | } 55 | 56 | private fun deleteAll(path: String) { 57 | deleteAll(f = File(path)) 58 | } 59 | 60 | private fun deleteAll(f: File) { 61 | runCatching { 62 | if (!f.exists()) return 63 | if (f.isFile) { 64 | f.delete() 65 | } else { 66 | f.listFiles().let { arr -> 67 | arr?.forEach { 68 | deleteAll(it) 69 | } ?: f.delete() 70 | } 71 | } 72 | }.logeIfThrow() 73 | } 74 | 75 | fun getConfigDir(): File { 76 | val path = "${CommonPath.publicData.second}/qqcleaner" 77 | val f = File(path) 78 | if (f.exists()) return f 79 | f.mkdir() 80 | return f 81 | } 82 | 83 | fun getAllConfigsAsync(onFinish: (Array) -> Unit) { 84 | thread { 85 | val arr = getAllConfigs() 86 | onFinish(arr) 87 | } 88 | } 89 | 90 | fun getAllConfigs(): Array { 91 | val arr = ArrayList() 92 | runCatching { 93 | getConfigDir().listFiles()?.let { 94 | it.forEach { f -> 95 | runCatching { 96 | arr.add(CleanData(f)) 97 | }.logeIfThrow() 98 | } 99 | } 100 | }.logeIfThrow() 101 | return arr.toTypedArray() 102 | } 103 | 104 | fun isConfigEmpty(): Boolean { 105 | return getConfigDir().listFiles()?.isEmpty() ?: true 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/ConfigManager.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import android.content.Context 4 | import com.github.kyuubiran.ezxhelper.init.InitFields.appContext 5 | 6 | object ConfigManager { 7 | val sPrefs by lazy { 8 | appContext.getSharedPreferences("qqcleaner", Context.MODE_PRIVATE) 9 | } 10 | 11 | fun putBool(key: String, value: Boolean) { 12 | sPrefs.edit().putBoolean(key, value).apply() 13 | } 14 | 15 | fun getBool(key: String, defValue: Boolean = false): Boolean { 16 | return sPrefs.getBoolean(key, defValue) 17 | } 18 | 19 | fun putString(key: String, value: String) { 20 | sPrefs.edit().putString(key, value).apply() 21 | } 22 | 23 | fun getString(key: String, defValue: String = ""): String { 24 | return sPrefs.getString(key, defValue) ?: defValue 25 | } 26 | 27 | fun putStringSet(key: String, value: Set) { 28 | sPrefs.edit().putStringSet(key, value).apply() 29 | } 30 | 31 | fun getStringSet(key: String, defValue: Set = emptySet()): Set { 32 | return sPrefs.getStringSet(key, defValue) ?: emptySet() 33 | } 34 | 35 | fun putInt(key: String, value: Int) { 36 | sPrefs.edit().putInt(key, value).apply() 37 | } 38 | 39 | fun getInt(key: String, defValue: Int = 0): Int { 40 | return sPrefs.getInt(key, defValue) 41 | } 42 | 43 | fun putFloat(key: String, value: Float) { 44 | sPrefs.edit().putFloat(key, value).apply() 45 | } 46 | 47 | fun getFloat(key: String, defValue: Float = 0.0f): Float { 48 | return sPrefs.getFloat(key, defValue) 49 | } 50 | 51 | fun putLong(key: String, value: Long) { 52 | sPrefs.edit().putLong(key, value).apply() 53 | } 54 | 55 | fun getLong(key: String, defValue: Long = 0L): Long { 56 | return sPrefs.getLong(key, defValue) 57 | } 58 | 59 | var sAutoClean: Boolean 60 | set(value) = putBool("auto_clean", value) 61 | get() = getBool("auto_clean") 62 | 63 | var sAutoCleanInterval: Int 64 | set(value) = putInt("auto_clean_interval", value) 65 | get() = getInt("auto_clean_interval", 24) 66 | 67 | var sLastCleanDate 68 | set(value) = putLong("last_clean_date", value) 69 | get() = getLong("last_clean_date") 70 | 71 | var sTotalCleaned: Long 72 | set(value) = putLong("total_cleaned", value) 73 | get() = getLong("total_cleaned") 74 | 75 | var sSilenceClean: Boolean 76 | set(value) = putBool("silence_clean", value) 77 | get() = getBool("silence_clean") 78 | 79 | var sThemeSelect: Int 80 | set(value) = putInt("theme_select", value) 81 | get() = getInt("theme_select") 82 | 83 | var sIsBlackTheme: Boolean 84 | set(value) = putBool("is_black_theme", value) 85 | get() = getBool("is_black_theme") 86 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/DateUtils.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import android.icu.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | private fun isBelong(beg: String, end: String): Boolean { 7 | val df = SimpleDateFormat("HH:mm", Locale.getDefault()) 8 | val cNow = Calendar.getInstance().apply { 9 | time = df.parse(df.format(Date())) 10 | } 11 | val cBeg = Calendar.getInstance().apply { 12 | time = df.parse(beg) 13 | } 14 | val cEnd = Calendar.getInstance().apply { 15 | time = df.parse(end) 16 | } 17 | return cNow.before(cEnd) && cNow.after(cBeg) 18 | } 19 | 20 | /** 21 | * 获取当前时间对应的标语 22 | */ 23 | fun getCurrentTimeText(): String { 24 | return when { 25 | isBelong("00:00", "04:59") -> "夜深了," 26 | isBelong("05:00", "10:59") -> "早上好," 27 | isBelong("11:00", "12:59") -> "中午好," 28 | isBelong("13:00", "17:59") -> "下午好," 29 | isBelong("18:00", "23:59") -> "晚上好," 30 | else -> "" 31 | } 32 | } 33 | 34 | fun getLastCleanTimeText(time: Long): String { 35 | return SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()).format(Date(time)) 36 | } 37 | 38 | fun getFormatCleanTimeText(time: Long): String { 39 | val diff = System.currentTimeMillis() - time 40 | var days = diff / 86400000L 41 | if (days == 0L) { 42 | val calendar = Calendar.getInstance() 43 | calendar.time = Date() 44 | val weekday1 = calendar.get(Calendar.DAY_OF_WEEK) 45 | calendar.time = Date(time) 46 | val weekday2 = calendar.get(Calendar.DAY_OF_WEEK) 47 | days += (weekday1 - weekday2) 48 | } 49 | return when (days) { 50 | 0L -> " 今天" 51 | 1L -> " 昨天" 52 | 2L -> " 前天" 53 | in 3L..Long.MAX_VALUE -> " $days 天前" 54 | else -> "未来的 ${-days} 天后" 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/HostApp.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import com.github.kyuubiran.ezxhelper.init.InitFields 4 | 5 | enum class HostApp { 6 | QQ, TIM, WE_CHAT 7 | } 8 | 9 | val HostApp.isQq: Boolean 10 | get() = hostApp == HostApp.QQ 11 | 12 | val HostApp.isTim: Boolean 13 | get() = hostApp == HostApp.TIM 14 | 15 | val HostApp.isQqOrTim: Boolean 16 | get() = hostApp == HostApp.QQ || hostApp == HostApp.TIM 17 | 18 | val HostApp.isWeChat: Boolean 19 | get() = hostApp == HostApp.WE_CHAT 20 | 21 | lateinit var hostApp: HostApp 22 | 23 | val hostAppName: String 24 | get() = when (hostApp) { 25 | HostApp.QQ -> "QQ" 26 | HostApp.TIM -> "TIM" 27 | HostApp.WE_CHAT -> "WECHAT" 28 | } 29 | 30 | 31 | object HostAppUtil { 32 | fun isCurrentHostAppByPackageName(packageName: String): Boolean { 33 | return packageName == InitFields.hostPackageName 34 | } 35 | 36 | fun containsCurrentHostApp(appName: String): Boolean { 37 | return appName.contains(hostAppName, ignoreCase = true) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/ImmersionBar.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import android.app.Activity 4 | import android.graphics.Color.TRANSPARENT 5 | import android.os.Build 6 | import android.os.Build.VERSION.SDK_INT 7 | import android.os.Build.VERSION_CODES.Q 8 | import android.os.Build.VERSION_CODES.R 9 | import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 10 | import android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 11 | import android.view.Window 12 | import android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS 13 | import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS 14 | import android.view.WindowManager.LayoutParams.* 15 | 16 | /** 17 | * 状态栏透明 18 | */ 19 | @Suppress("DEPRECATION") 20 | fun Window.setStatusBarTranslation() { 21 | if (SDK_INT >= Q) 22 | this.isStatusBarContrastEnforced = false 23 | // 设置状态栏透明,暂时没有更好的办法解决透明问题 24 | this.addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 25 | this.clearFlags(FLAG_TRANSLUCENT_STATUS) 26 | this.statusBarColor = TRANSPARENT 27 | } 28 | 29 | /** 30 | * 亮色状态栏 31 | * @param enable 默认为亮色 32 | */ 33 | fun Activity.statusBarLightMode(enable: Boolean = true) { 34 | this.window.setStatusBarTranslation() 35 | if (SDK_INT >= R) 36 | window.insetsController?.setSystemBarsAppearance( 37 | if (enable) APPEARANCE_LIGHT_STATUS_BARS else 0, 38 | APPEARANCE_LIGHT_STATUS_BARS 39 | ) 40 | this.statusBarLightOldMode(enable) 41 | } 42 | 43 | /** 44 | * 亮色状态栏(对应旧版本) 45 | * @param enable 默认为亮色 46 | */ 47 | @Suppress("DEPRECATION") 48 | fun Activity.statusBarLightOldMode(enable: Boolean = true) { 49 | window.decorView.systemUiVisibility = if (enable) 50 | window.decorView.systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 51 | else 52 | window.decorView.systemUiVisibility xor SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 53 | } 54 | 55 | /** 56 | * 导航栏透明 57 | */ 58 | @Suppress("DEPRECATION") 59 | fun Window.setNavigationBarTranslation() { 60 | // 防止透明以后高对比度 61 | if (SDK_INT >= Q) 62 | this.isNavigationBarContrastEnforced = false 63 | // 去除导航栏线段 64 | if (SDK_INT >= Build.VERSION_CODES.P) 65 | this.navigationBarDividerColor = TRANSPARENT 66 | // 设置导航栏透明,暂时没有更好的办法解决透明问题 67 | this.addFlags(FLAG_TRANSLUCENT_NAVIGATION) 68 | this.navigationBarColor = TRANSPARENT 69 | } 70 | 71 | /** 72 | * 亮色导航栏 73 | * @param enable 默认为亮色 74 | */ 75 | @Suppress("DEPRECATION") 76 | fun Activity.navigationBarMode(enable: Boolean = true) { 77 | this.window.setNavigationBarTranslation() 78 | if (SDK_INT >= R) 79 | window.insetsController?.setSystemBarsAppearance( 80 | if (enable) APPEARANCE_LIGHT_NAVIGATION_BARS else 0, 81 | APPEARANCE_LIGHT_NAVIGATION_BARS 82 | ) 83 | this.navigationBarOldMode(enable) 84 | } 85 | 86 | /** 87 | * 亮色导航栏(对应旧版本) 88 | * @param enable 默认为亮色 89 | */ 90 | @Suppress("DEPRECATION") 91 | fun Activity.navigationBarOldMode(enable: Boolean = true) { 92 | window.decorView.systemUiVisibility = if (enable) 93 | window.decorView.systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 94 | else 95 | window.decorView.systemUiVisibility xor SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 96 | 97 | } 98 | 99 | 100 | -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/PathUtil.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import me.kyuubiran.qqcleaner.data.CleanData 4 | import me.kyuubiran.qqcleaner.util.path.CommonPath 5 | import me.kyuubiran.qqcleaner.util.path.QQPath 6 | import me.kyuubiran.qqcleaner.util.path.WeChatPath 7 | 8 | object PathUtil { 9 | /** 10 | * @throws IllegalArgumentException if [path] is not a valid path 11 | */ 12 | fun getFullPath(path: CleanData.PathData.Path): String { 13 | var tmp = path.suffix 14 | 15 | when (path.prefix) { 16 | CommonPath.publicData.first -> 17 | tmp = CommonPath.publicData.second + path.suffix 18 | CommonPath.privateData.first -> 19 | tmp = CommonPath.privateData.second + path.suffix 20 | QQPath.tencentDir.first -> 21 | if (hostApp.isQqOrTim) tmp = QQPath.tencentDir.second + path.suffix 22 | WeChatPath.publicUserData.first -> 23 | if (hostApp.isWeChat) tmp = WeChatPath.publicUserData.second + path.suffix 24 | WeChatPath.privateUserData.first -> 25 | if (hostApp.isWeChat) tmp = WeChatPath.privateUserData.second + path.suffix 26 | } 27 | 28 | if (tmp == path.suffix) throw IllegalArgumentException("Unsupported path prefix: ${path.prefix}") 29 | return tmp 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.icu.math.BigDecimal 6 | import android.net.Uri 7 | import android.os.Parcelable 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.navigation.NavController 12 | import java.io.Serializable 13 | 14 | @Composable 15 | fun rememberMutableStateOf(value: T) = remember { 16 | mutableStateOf(value) 17 | } 18 | 19 | fun Context.jumpUri(uriString: String) { 20 | this.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uriString))) 21 | } 22 | 23 | fun Context.jumpUri(uri: Uri) { 24 | this.startActivity(Intent(Intent.ACTION_VIEW, uri)) 25 | } 26 | 27 | inline fun tryOr(defValue: T, block: () -> T): T { 28 | return try { 29 | block() 30 | } catch (thr: Throwable) { 31 | defValue 32 | } 33 | } 34 | 35 | fun NavController.getParcelableArgument(key: String): T? { 36 | return this.currentBackStackEntry?.arguments?.getParcelable(key) 37 | } 38 | 39 | fun NavController.getSerializableArgument(key: String): T? { 40 | @Suppress("UNCHECKED_CAST") 41 | return this.currentBackStackEntry?.arguments?.getSerializable(key) as T? 42 | } 43 | 44 | fun getFormatCleanedSize(): String { 45 | val cleaned = ConfigManager.sTotalCleaned 46 | val sl = BigDecimal(cleaned) 47 | val b: BigDecimal 48 | val result: Double 49 | return when (cleaned.toString().length) { 50 | in 0..3 -> { 51 | "$sl Byte" 52 | } 53 | in 4..6 -> { 54 | b = sl.divide(BigDecimal(1_024.0)) 55 | result = b.setScale(2, BigDecimal.ROUND_HALF_UP).toDouble() 56 | "$result KiB" 57 | } 58 | in 7..9 -> { 59 | b = sl.divide(BigDecimal(1_048_576.0)) 60 | result = b.setScale(2, BigDecimal.ROUND_HALF_UP).toDouble() 61 | "$result MiB" 62 | } 63 | in 10..12 -> { 64 | b = sl.divide(BigDecimal(1_073_741_824.0)) 65 | result = b.setScale(2, BigDecimal.ROUND_HALF_UP).toDouble() 66 | "$result GiB" 67 | } 68 | in 12..99 -> { 69 | b = sl.divide(BigDecimal(1_099_511_627_776.0)) 70 | result = b.setScale(2, BigDecimal.ROUND_HALF_UP).toDouble() 71 | "$result TiB" 72 | } 73 | else -> { 74 | "" 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/path/CommonPath.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util.path 2 | 3 | import com.github.kyuubiran.ezxhelper.init.InitFields.appContext 4 | 5 | object CommonPath { 6 | //P: storage/emulated/0/Android/data/${HostAppPackageName} 7 | val publicData by lazy { 8 | "!PublicDataDir" to (appContext.externalCacheDir?.parentFile?.path ?: "") 9 | } 10 | 11 | //P: data/user/0/${HostAppPackageName} 12 | val privateData by lazy { 13 | "!PrivateDataDir" to (appContext.filesDir?.parentFile?.path ?: "") 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/path/QQPath.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util.path 2 | 3 | import com.github.kyuubiran.ezxhelper.init.InitFields.appContext 4 | 5 | object QQPath { 6 | //P: storage/emulated/0/tencent 7 | val tencentDir by lazy { 8 | "!TencentDir" to (appContext.obbDir?.parentFile?.parentFile?.parentFile?.path?.let { "${it}/tencent" } 9 | ?: "") 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/path/TIMPath.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util.path 2 | 3 | import com.github.kyuubiran.ezxhelper.init.InitFields.appContext 4 | 5 | object TIMPath { 6 | //P: storage/emulated/0/tencent 7 | val tencentDir by lazy { 8 | "!TencentDir" to (appContext.obbDir?.parentFile?.parentFile?.parentFile?.path?.let { "${it}/tencent" } 9 | ?: "") 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/me/kyuubiran/qqcleaner/util/path/WeChatPath.kt: -------------------------------------------------------------------------------- 1 | package me.kyuubiran.qqcleaner.util.path 2 | 3 | import java.io.File 4 | 5 | object WeChatPath { 6 | //P: storage/emulated/0/Android/data/com.tencent.mm/MicroMsg/${UserDataDirName} 7 | val publicUserData: Pair by lazy { 8 | "!PublicUserDataDir" to run { 9 | val dirs = File("${CommonPath.publicData.second}/MicroMsg").listFiles() 10 | if (dirs != null && dirs.isNotEmpty()) { 11 | return@run dirs.firstOrNull { it.name.length == 32 && it.isDirectory }?.absolutePath 12 | ?: "" 13 | } 14 | "" 15 | } 16 | } 17 | 18 | //P: data/user/0/com.tencent.mm/MicroMsg/${UserDataDirName} 19 | val privateUserData: Pair by lazy { 20 | "!PrivateUserDataDir" to run { 21 | val dirs = File("${CommonPath.privateData.second}/MicroMsg").listFiles() 22 | if (dirs != null && dirs.isNotEmpty()) { 23 | return@run dirs.firstOrNull { 24 | it.name.length == 32 && it.isDirectory 25 | && File("${it.absolutePath}/account.bin").exists() 26 | }?.absolutePath ?: "" 27 | } 28 | "" 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_developer_agoines.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/app/src/main/res/drawable-xxxhdpi/ic_developer_agoines.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_developer_ketal.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/app/src/main/res/drawable-xxxhdpi/ic_developer_ketal.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_developer_kitsunepie.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/app/src/main/res/drawable-xxxhdpi/ic_developer_kitsunepie.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_developer_kyuubiran.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/app/src/main/res/drawable-xxxhdpi/ic_developer_kyuubiran.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_developer_maitungtm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/app/src/main/res/drawable-xxxhdpi/ic_developer_maitungtm.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_developer_nextalone.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/app/src/main/res/drawable-xxxhdpi/ic_developer_nextalone.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_a.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_android.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chosen.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cilpboard.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_copy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_default.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit_name.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_qqcleaner.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home_qqcleaner_dark.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list_empty.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list_empty_dark.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_moon.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sun.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_default_off_to_on.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_default_off_to_on_drak.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_default_on_to_off.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_default_on_to_off_drak.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_off_to_on_white.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 57 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_off_to_on_white_drak.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 57 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_on_to_off_white.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 36 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 58 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/switch_on_to_off_white_drak.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 36 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 58 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.tencent.mobileqq 6 | com.tencent.tim 7 | com.tencent.mm 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 设置->关于-> QQ / TIM / 微信瘦身 3 | QQ瘦身 4 | 瘦身模块 5 | 确认 6 | 取消 7 | 8 | 上次瘦身是%s 9 | 设定 10 | 立即瘦身 11 | QQ瘦身 logo 12 | 单位:h(默认 24) 13 | 上次瘦身 %s 14 | 暂时还没有瘦身记录 15 | 暂无瘦身记录 16 | 更多 17 | 主题风格 18 | 浅色主题 19 | 深色主题 20 | 跟随系统 21 | 使用纯黑色深色主题 22 | 关于瘦身 23 | 24 | 编辑配置 25 | 编辑配置信息 26 | 执行 27 | 导出至 Download 目录 28 | 复制到剪贴板 29 | 删除 30 | 31 | 添加配置 32 | 添加类别 33 | 新建配置 34 | 启用配置 35 | 选择本地文件 36 | 导入默认配置 37 | 已导入默认配置 38 | 从剪贴板导入 39 | 已从剪贴板导入“%s”! 40 | 导入失败,请检查剪贴板内容或是否已授予 QQ 或微信「读取剪贴板内容」权限。 41 | 42 | 作者 %s 43 | 读取配置文件“%s”时发生错误,请检查配置文件是否完整。 44 | 正在瘦身… 45 | 执行配置文件“%s”时发生错误,已跳过剩余部分。 46 | 自动瘦身 47 | 静默自动瘦身 48 | 已关闭静默瘦身 49 | 执行自动瘦身任务时将不会弹出 Toast 50 | 自动瘦身间隔 51 | "%d" h 52 | 瘦身配置 53 | 瘦身配置 54 | 设置自动瘦身时长 55 | 编辑配置信息 56 | 取消 57 | 正在执行配置文件:%s 58 | 瘦身失败,你还没有启用任何配置文件! 59 | 已应用对配置文件“%s”的更改。 60 | 开发者 61 | 关于 62 | 空视图 63 | 删除确定 64 | 65 | 点击按钮添加路径 66 | 组织 67 | KitsunePie 68 | 画大饼 69 | 70 | KyuubiRan 71 | 主要开发者 72 | Ketal 73 | 开发者 74 | NextAlone 75 | 开发者 76 | Agoines 77 | UI 开发者 78 | MaiTungTM 79 | UI & Icon 设计师 80 | 81 | Github 82 | Telegram 频道 83 | Telegram 群组 84 | 添加路径 85 | 编辑路径 86 | 修改路径类别名称 87 | 路径类别名称 88 | 您确定要删除「%s」吗? 89 | 90 | 空空如也 91 | 添加路径 92 | 文件夹图标 93 | 瘦身完毕! 94 | 95 | 96 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | val gradleVersion = "7.1.2" 7 | val kotlinVersion = "1.6.10" 8 | dependencies { 9 | classpath("com.android.tools.build:gradle:$gradleVersion") 10 | classpath(kotlin("gradle-plugin", version = kotlinVersion)) 11 | } 12 | } 13 | 14 | 15 | tasks.register("clean").configure { 16 | delete(rootProject.buildDir) 17 | } 18 | -------------------------------------------------------------------------------- /clean-config/README.md: -------------------------------------------------------------------------------- 1 | ## 这是什么? 2 | 3 | 这个位置存储着通过审核的瘦身配置文件,由于是经过验证的,所以可以放心使用,大家可以通过下载配置导入或者是直接复制的方式在瘦身中使用。 4 | 5 | ## 如何向本项目添加你的配置文件? 6 | 7 | 如果你也想贡献你的配置文件,你需要先fork本仓库并向指定目录添加自己的配置文件,然后创建拉取请求,通过审核后你的配置文件就会保存在这个目录里。 8 | 9 | ## 字段说明 10 | CleanData: 11 | | 字段 | title | author | enable | hostApp | content | 12 | | :------: | :-----: | :------: | :-----: | :-----: | :-----: | 13 | | **说明** | 配置标题 | 作者昵称 | 是否启用 | 宿主类型 | 瘦身路径 | 14 | | **类型** | String | String | Boolean | String | Array\ | 15 | 16 | Content: 17 | | 字段 | title | enable | path | 18 | | :------: | :-----: | :-----: | :------: | 19 | | **说明** | 子类标题 | 是否启用 | 路径列表 | 20 | | **类型** | String | Boolean | Array\ | 21 | 22 | Path: 23 | | 字段 | prefix | suffix | 24 | | :------: | :-----: | :-----: | 25 | | **说明** | 路径前缀 | 路径后缀 | 26 | | **类型** | String | String | 27 | 28 | **注:前缀(相对路径)请看相对目录表,后缀则是完整路径减去前缀并以**`/`**开头** 29 | 30 | **宿主类型:** 31 | - qq 32 | - tim 33 | - wechat 34 | 35 | 若需要支持多个可以用任意符号分割,如`qq/tim`为支持QQ/TIM的配置文件,但是如果在通用配置文件中使用了不支持的前缀,可能会导致严重的问题。 36 | -------------------------------------------------------------------------------- /clean-config/qq/README.md: -------------------------------------------------------------------------------- 1 | ## 这是什么? 2 | 3 | 这个位置存储着QQ/TIM的瘦身配置文件。 默认存储已移动至[此处](../../app/src/main/assets/qq.json) 4 | 5 | ## 相对目录表 6 | 7 | !PublicDataDir -> storage/emulated/0/Android/data/com.tencent.mobileqq 8 | !PrivateDataDir -> data/user/0/com.tencent.mobileqq 9 | !TencentDir -> storage/emulated/0/tencent 10 | -------------------------------------------------------------------------------- /clean-config/wechat/README.md: -------------------------------------------------------------------------------- 1 | ## 这是什么? 2 | 3 | 这个位置存储着微信的瘦身配置文件。 默认存储已移动至[此处](../../app/src/main/assets/wechat.json) 4 | 5 | ## 相对目录表 6 | 7 | !PublicDataDir -> storage/emulated/0/Android/data/com.tencent.mm 8 | !PrivateDataDir -> data/user/0/com.tencent.mm 9 | !PublicUserData -> storage/emulated/0/Android/data/com.tencent.mm/MicroMsg/用户数据文件夹(32个英文数字字符) 10 | !PrivateUserData -> data/user/0/com.tencent.mm/MicroMsg/用户数据文件夹(32个英文数字字符且存在account.bin文件) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8192m 2 | android.useAndroidX=true 3 | android.nonTransitiveRClass=true 4 | kotlin.code.style=official 5 | android.enableR8.fullMode=true 6 | android.enableAppCompileTimeRClass=true 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 12 19:46:05 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /image/Project_QQCleaner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KitsunePie/QQCleaner/bba0f606308aadec64397c2acd15b598bf593cbb/image/Project_QQCleaner.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 3 | repositories { 4 | google() 5 | mavenCentral() 6 | maven("https://api.xposed.info/") 7 | maven("https://jitpack.io") 8 | } 9 | } 10 | 11 | include(":app") 12 | rootProject.name = "QQ瘦身" 13 | --------------------------------------------------------------------------------