├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ ├── com.sanmer.mrepo.database.ModuleDatabase │ │ └── 1.json │ └── com.sanmer.mrepo.database.RepoDatabase │ │ ├── 1.json │ │ └── 2.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── sanmer │ │ └── mrepo │ │ └── provider │ │ └── ISuProvider.aidl │ ├── kotlin │ └── com │ │ └── sanmer │ │ └── mrepo │ │ ├── App.kt │ │ ├── api │ │ ├── ApiInitializerListener.kt │ │ ├── local │ │ │ ├── KernelSuModulesApi.kt │ │ │ ├── MagiskModulesApi.kt │ │ │ └── ModulesLocalApi.kt │ │ └── online │ │ │ └── ModulesRepoApi.kt │ │ ├── app │ │ ├── Const.kt │ │ ├── event │ │ │ ├── Event.kt │ │ │ └── State.kt │ │ └── utils │ │ │ ├── MediaStoreUtils.kt │ │ │ ├── NotificationUtils.kt │ │ │ └── ShortcutUtils.kt │ │ ├── database │ │ ├── ModuleDatabase.kt │ │ ├── RepoDatabase.kt │ │ ├── dao │ │ │ ├── ModuleDao.kt │ │ │ └── RepoDao.kt │ │ ├── di │ │ │ └── DatabaseModule.kt │ │ └── entity │ │ │ ├── LocalModule.kt │ │ │ ├── OnlineModule.kt │ │ │ └── Repo.kt │ │ ├── datastore │ │ ├── UserData.kt │ │ ├── UserPreferencesDataSource.kt │ │ ├── UserPreferencesSerializer.kt │ │ └── di │ │ │ └── DataStoreModule.kt │ │ ├── di │ │ ├── CoroutineDispatcherModule.kt │ │ ├── CoroutineQualifier.kt │ │ └── CoroutineScopeModule.kt │ │ ├── model │ │ ├── json │ │ │ ├── AppUpdate.kt │ │ │ ├── License.kt │ │ │ ├── ModuleUpdate.kt │ │ │ └── Modules.kt │ │ └── module │ │ │ ├── LocalModule.kt │ │ │ └── OnlineModule.kt │ │ ├── provider │ │ ├── SuProvider.kt │ │ ├── SuProviderImpl.kt │ │ └── di │ │ │ └── ProviderModule.kt │ │ ├── repository │ │ ├── LocalRepository.kt │ │ ├── ModulesRepository.kt │ │ ├── SuRepository.kt │ │ └── UserDataRepository.kt │ │ ├── service │ │ ├── DownloadService.kt │ │ └── LogcatService.kt │ │ ├── ui │ │ ├── activity │ │ │ ├── base │ │ │ │ └── BaseActivity.kt │ │ │ ├── install │ │ │ │ ├── InstallActivity.kt │ │ │ │ └── InstallScreen.kt │ │ │ ├── license │ │ │ │ ├── LicenseActivity.kt │ │ │ │ └── LicenseScreen.kt │ │ │ ├── log │ │ │ │ ├── LogActivity.kt │ │ │ │ └── LogScreen.kt │ │ │ └── main │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainScreen.kt │ │ │ │ └── SetupScreen.kt │ │ ├── animate │ │ │ ├── EnterTransition.kt │ │ │ └── ExitTransition.kt │ │ ├── component │ │ │ ├── AppBar.kt │ │ │ ├── Checkbox.kt │ │ │ ├── DropdownMenu.kt │ │ │ ├── ExpandableItem.kt │ │ │ ├── ModuleCard.kt │ │ │ ├── NormalChip.kt │ │ │ ├── PageIndicator.kt │ │ │ ├── SegmentedButtons.kt │ │ │ ├── SettingItem.kt │ │ │ └── Tab.kt │ │ ├── navigation │ │ │ ├── BottomNav.kt │ │ │ ├── Main.kt │ │ │ └── graph │ │ │ │ ├── Home.kt │ │ │ │ ├── Modules.kt │ │ │ │ └── Settings.kt │ │ ├── screens │ │ │ ├── apptheme │ │ │ │ ├── AppThemeScreen.kt │ │ │ │ ├── DarkModeItem.kt │ │ │ │ ├── ExampleItem.kt │ │ │ │ └── ThemePaletteItem.kt │ │ │ ├── home │ │ │ │ ├── AppUpdateItem.kt │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── InfoItem.kt │ │ │ │ ├── MenuItem.kt │ │ │ │ ├── NonRootItem.kt │ │ │ │ └── RootItem.kt │ │ │ ├── modules │ │ │ │ ├── MenuItem.kt │ │ │ │ ├── ModulesScreen.kt │ │ │ │ ├── SegmentedButtonsItem.kt │ │ │ │ ├── TabsItem.kt │ │ │ │ └── pages │ │ │ │ │ ├── CloudPage.kt │ │ │ │ │ ├── InstalledPage.kt │ │ │ │ │ └── UpdatablePage.kt │ │ │ ├── repository │ │ │ │ ├── RepoItem.kt │ │ │ │ └── RepositoryScreen.kt │ │ │ ├── settings │ │ │ │ ├── DownloadPathItem.kt │ │ │ │ └── SettingsScreen.kt │ │ │ └── viewmodule │ │ │ │ ├── ChangelogItem.kt │ │ │ │ ├── ModuleInfoItem.kt │ │ │ │ ├── VersionsItem.kt │ │ │ │ └── ViewModuleScreen.kt │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ ├── Type.kt │ │ │ └── color │ │ │ │ ├── Blue.kt │ │ │ │ ├── Cyan.kt │ │ │ │ ├── DeepPurple.kt │ │ │ │ ├── Orange.kt │ │ │ │ └── Sakura.kt │ │ └── utils │ │ │ ├── LazyListState.kt │ │ │ ├── Logo.kt │ │ │ ├── NavController.kt │ │ │ ├── NavigateUpTopBar.kt │ │ │ ├── PaddingValues.kt │ │ │ ├── Text.kt │ │ │ └── WindowInsets.kt │ │ ├── utils │ │ ├── HttpUtils.kt │ │ ├── ModuleUtils.kt │ │ ├── SvcPower.kt │ │ ├── expansion │ │ │ ├── Context.kt │ │ │ ├── List.kt │ │ │ ├── LocalDateTime.kt │ │ │ ├── Parcelable.kt │ │ │ ├── Response.kt │ │ │ ├── Result.kt │ │ │ ├── String.kt │ │ │ └── ZipUtils.kt │ │ ├── log │ │ │ ├── LogText.kt │ │ │ └── Logcat.kt │ │ └── timber │ │ │ ├── DebugTree.kt │ │ │ └── ReleaseTree.kt │ │ ├── viewmodel │ │ ├── DetailViewModel.kt │ │ ├── HomeViewModel.kt │ │ ├── InstallViewModel.kt │ │ ├── ModulesViewModel.kt │ │ └── RepositoryViewModel.kt │ │ └── works │ │ ├── LocalWork.kt │ │ └── RepoWork.kt │ ├── playstore.png │ ├── proto │ └── com │ │ └── sanmer │ │ └── mrepo │ │ └── datastore │ │ └── UserPreferences.proto │ └── res │ ├── drawable │ ├── add_outline.xml │ ├── arrow_down_bold.xml │ ├── arrow_right_bold.xml │ ├── arrow_square_left_outline.xml │ ├── auto_brightness_outline.xml │ ├── box_bold.xml │ ├── box_outline.xml │ ├── box_remove_outline.xml │ ├── bucket_outline.xml │ ├── close_square_outline.xml │ ├── cloud_change_outline.xml │ ├── cloud_connection_outline.xml │ ├── cube_scan_outline.xml │ ├── danger_outline.xml │ ├── directbox_receive_outline.xml │ ├── document_code_outline.xml │ ├── document_text_outline.xml │ ├── flag_outline.xml │ ├── health_bold.xml │ ├── health_outline.xml │ ├── hierarchy_outline.xml │ ├── home_bold.xml │ ├── home_outline.xml │ ├── ic_launcher_outline.xml │ ├── ic_logo.xml │ ├── import_outline.xml │ ├── information_outline.xml │ ├── link_outline.xml │ ├── link_square_outline.xml │ ├── main_component_outline.xml │ ├── mobile_outline.xml │ ├── moon_outline.xml │ ├── osi.xml │ ├── people_bold.xml │ ├── refresh_outline.xml │ ├── rotate_left_outline.xml │ ├── rotate_outline.xml │ ├── search_normal_outline.xml │ ├── send_outline.xml │ ├── setting_bold.xml │ ├── setting_outline.xml │ ├── shortcut_log.xml │ ├── shortcut_modules.xml │ ├── shortcut_settings.xml │ ├── slash_outline.xml │ ├── sort_outline.xml │ ├── square_outline.xml │ ├── star_outline.xml │ ├── sun_outline.xml │ ├── tick_circle_bold.xml │ ├── tick_circle_outline.xml │ ├── tick_square_bold.xml │ ├── translate_outline.xml │ └── trash_outline.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── mipmap-anydpi-v33 │ └── ic_launcher.xml │ ├── values-ar │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ro │ └── strings.xml │ ├── values-v31 │ └── colors.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ ├── strings_untranslatable.xml │ └── themes.xml │ └── xml │ ├── locales_config.xml │ └── provider_paths.xml ├── build-logic ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── AndroidApplicationComposeConventionPlugin.kt │ ├── AndroidApplicationConventionPlugin.kt │ ├── AndroidHiltConventionPlugin.kt │ └── AndroidRoomConventionPlugin.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── de │ ├── full_description.txt │ └── short_description.txt │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ └── short_description.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | release/ 19 | debug/ 20 | 21 | # Gradle files 22 | .gradle/ 23 | build/ 24 | 25 | # Local configuration file (sdk path, etc) 26 | local.properties 27 | 28 | # Proguard folder generated by Eclipse 29 | proguard/ 30 | 31 | # Log Files 32 | *.log 33 | 34 | # Android Studio Navigation editor temp files 35 | .navigation/ 36 | 37 | # Android Studio captures folder 38 | captures/ 39 | 40 | # IntelliJ 41 | *.iml 42 | .idea/ 43 | 44 | # Keystore files 45 | # Uncomment the following lines if you do not want to check your keystore files in. 46 | #*.jks 47 | #*.keystore 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | .cxx/ 52 | 53 | # Google Services (e.g. APIs or Firebase) 54 | #google-services.json 55 | 56 | # Freeline 57 | freeline.py 58 | freeline/ 59 | freeline_project_description.json 60 | 61 | # fastlane 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | fastlane/readme.md 67 | 68 | # Version control 69 | vcs.xml 70 | 71 | # lint 72 | lint/intermediates/ 73 | lint/generated/ 74 | lint/outputs/ 75 | lint/tmp/ 76 | # lint/reports/ 77 | 78 | # Mac OS 79 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MRepo 2 | [![release](https://img.shields.io/github/v/release/ya0211/MRepo?label=release&color=red)](https://github.com/ya0211/MRepo/releases) [![download](https://shields.io/github/downloads/ya0211/MRepo/total?label=download)](https://github.com/ya0211/MRepo/releases) [![license](https://img.shields.io/github/license/ya0211/MRepo)](LICENSE) [![follow](https://img.shields.io/badge/Follow-Telegram-blue.svg?label=follow)](https://t.me/mrepo_news) [![translated](https://weblate.sanmer.dev/widgets/mrepo/-/svg-badge.svg)](https://weblate.sanmer.dev/engage/mrepo/) 3 | 4 | MRepo (short for `My Repository` or `Modules(Magisk) Repository`) is an Android app that helps manage your own modules repository. 5 | 6 | MRepo is written with [Jetpack Compose](https://developer.android.com/jetpack/compose). 7 | 8 | ## Preview 9 |

10 |

11 | 12 | ## Features 13 | - Jetpack Compose & Material Design 3 14 | - Download and update modules 15 | - Your own modules repository 16 | - Support multiple repositories 17 | 18 | ## Supported Versions 19 | - Android 8.0 ~ 13 20 | - Magisk 24.0 ~ latest 21 | - KernelSU 0.5.0 ~ latest 22 | 23 | ## Modules Repository 24 | - [magisk-modules-repo-util](https://github.com/ya0211/magisk-modules-repo-util): the util to help build modules repository 25 | - [ya0211/magisk-modules-alt-repo](https://github.com/ya0211/magisk-modules-alt-repo): a mirror of Magisk-Modules-Alt-Repo 26 | 27 | ## Credits 28 | - [iconsax](https://iconsax.io): the icons of the Vuesax framework 29 | 30 | ## License 31 | 32 | Copyright (C) 2022 Sanmer 33 | 34 | This program is free software: you can redistribute it and/or modify 35 | it under the terms of the GNU General Public License as published by 36 | the Free Software Foundation, either version 3 of the License, or 37 | (at your option) any later version. 38 | 39 | This program is distributed in the hope that it will be useful, 40 | but WITHOUT ANY WARRANTY; without even the implied warranty of 41 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 42 | GNU General Public License for more details. 43 | 44 | You should have received a copy of the GNU General Public License 45 | along with this program. If not, see . 46 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -verbose 2 | -dontpreverify 3 | -optimizationpasses 5 4 | -dontskipnonpubliclibraryclasses 5 | 6 | -dontwarn org.conscrypt.** 7 | -dontwarn kotlinx.serialization.** 8 | 9 | # Keep DataStore fields 10 | -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* { 11 | ; 12 | } 13 | 14 | -repackageclasses com.sanmer.mrepo 15 | 16 | # TODO: Waiting for new retrofit release to remove these rules 17 | -keep,allowobfuscation,allowshrinking interface retrofit2.Call 18 | -keep,allowobfuscation,allowshrinking class retrofit2.Response 19 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation -------------------------------------------------------------------------------- /app/schemas/com.sanmer.mrepo.database.ModuleDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "0fab516c3fa6b3eee480e4ee8a75160e", 6 | "entities": [ 7 | { 8 | "tableName": "local_module", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `version` TEXT NOT NULL, `version_code` INTEGER NOT NULL, `author` TEXT NOT NULL, `description` TEXT NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "version", 25 | "columnName": "version", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "versionCode", 31 | "columnName": "version_code", 32 | "affinity": "INTEGER", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "author", 37 | "columnName": "author", 38 | "affinity": "TEXT", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "description", 43 | "columnName": "description", 44 | "affinity": "TEXT", 45 | "notNull": true 46 | }, 47 | { 48 | "fieldPath": "state", 49 | "columnName": "state", 50 | "affinity": "INTEGER", 51 | "notNull": true 52 | } 53 | ], 54 | "primaryKey": { 55 | "autoGenerate": false, 56 | "columnNames": [ 57 | "id" 58 | ] 59 | }, 60 | "indices": [], 61 | "foreignKeys": [] 62 | } 63 | ], 64 | "views": [], 65 | "setupQueries": [ 66 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 67 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0fab516c3fa6b3eee480e4ee8a75160e')" 68 | ] 69 | } 70 | } -------------------------------------------------------------------------------- /app/schemas/com.sanmer.mrepo.database.RepoDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "cc7c4d011b1e630a665fd83d84c653b1", 6 | "entities": [ 7 | { 8 | "tableName": "repo", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `name` TEXT NOT NULL, `size` INTEGER NOT NULL, `timestamp` REAL NOT NULL, `enable` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "url", 13 | "columnName": "url", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "name", 19 | "columnName": "name", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "size", 25 | "columnName": "size", 26 | "affinity": "INTEGER", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "timestamp", 31 | "columnName": "timestamp", 32 | "affinity": "REAL", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "enable", 37 | "columnName": "enable", 38 | "affinity": "INTEGER", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "id", 43 | "columnName": "id", 44 | "affinity": "INTEGER", 45 | "notNull": true 46 | } 47 | ], 48 | "primaryKey": { 49 | "columnNames": [ 50 | "id" 51 | ], 52 | "autoGenerate": false 53 | }, 54 | "indices": [], 55 | "foreignKeys": [] 56 | } 57 | ], 58 | "views": [], 59 | "setupQueries": [ 60 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 61 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cc7c4d011b1e630a665fd83d84c653b1')" 62 | ] 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/aidl/com/sanmer/mrepo/provider/ISuProvider.aidl: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.provider; 2 | 3 | interface ISuProvider { 4 | int getPid(); 5 | String getContext(); 6 | int getEnforce(); 7 | IBinder getFileSystemService(); 8 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/App.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.hilt.work.HiltWorkerFactory 6 | import androidx.work.Configuration 7 | import androidx.work.WorkManager 8 | import com.sanmer.mrepo.app.event.isNotReady 9 | import com.sanmer.mrepo.app.event.isSucceeded 10 | import com.sanmer.mrepo.app.utils.ShortcutUtils 11 | import com.sanmer.mrepo.di.MainScope 12 | import com.sanmer.mrepo.provider.SuProviderImpl 13 | import com.sanmer.mrepo.repository.UserDataRepository 14 | import com.sanmer.mrepo.utils.timber.DebugTree 15 | import com.sanmer.mrepo.utils.timber.ReleaseTree 16 | import com.sanmer.mrepo.works.LocalWork 17 | import com.sanmer.mrepo.works.RepoWork 18 | import dagger.hilt.android.HiltAndroidApp 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.flow.combine 21 | import kotlinx.coroutines.flow.launchIn 22 | import kotlinx.coroutines.flow.map 23 | import timber.log.Timber 24 | import javax.inject.Inject 25 | 26 | @HiltAndroidApp 27 | class App : Application(), Configuration.Provider { 28 | @Inject 29 | lateinit var workerFactory: HiltWorkerFactory 30 | 31 | @Inject 32 | lateinit var userDataRepository: UserDataRepository 33 | 34 | @Inject 35 | lateinit var suProviderImpl: SuProviderImpl 36 | 37 | @MainScope 38 | @Inject 39 | lateinit var mainScope: CoroutineScope 40 | 41 | private val workManger by lazy { WorkManager.getInstance(this) } 42 | 43 | init { 44 | if (BuildConfig.DEBUG) { 45 | Timber.plant(DebugTree()) 46 | } else { 47 | Timber.plant(ReleaseTree()) 48 | } 49 | } 50 | 51 | override fun onCreate() { 52 | super.onCreate() 53 | app = this 54 | 55 | ShortcutUtils.push() 56 | initSuProviderImpl() 57 | workManger.enqueue(RepoWork.OneTimeWork) 58 | } 59 | 60 | override fun getWorkManagerConfiguration() = 61 | Configuration.Builder() 62 | .setWorkerFactory(workerFactory) 63 | .build() 64 | 65 | private fun initSuProviderImpl() { 66 | userDataRepository.userData 67 | .map { it.isRoot } 68 | .combine(suProviderImpl.state) { isRoot, state -> 69 | if (state.isNotReady && isRoot) { 70 | suProviderImpl.init() 71 | } 72 | 73 | if (state.isSucceeded) { 74 | workManger.enqueue(LocalWork.OneTimeWork) 75 | } 76 | 77 | }.launchIn(mainScope) 78 | } 79 | 80 | companion object { 81 | private lateinit var app: App 82 | val context: Context get() = app 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/api/ApiInitializerListener.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.api 2 | 3 | interface ApiInitializerListener { 4 | fun onSuccess() 5 | fun onFailure() 6 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/api/local/ModulesLocalApi.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.api.local 2 | 3 | import com.sanmer.mrepo.model.module.LocalModule 4 | import java.io.File 5 | 6 | interface ModulesLocalApi { 7 | val version: String 8 | 9 | suspend fun getModules(): Result> 10 | 11 | fun enable(module: LocalModule) 12 | 13 | fun disable(module: LocalModule) 14 | 15 | fun remove(module: LocalModule) 16 | 17 | fun install( 18 | console: (String) -> Unit, 19 | onSuccess: (LocalModule) -> Unit, 20 | onFailure: () -> Unit, 21 | zipFile: File 22 | ) 23 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/api/online/ModulesRepoApi.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.api.online 2 | 3 | import com.sanmer.mrepo.BuildConfig 4 | import com.sanmer.mrepo.model.json.ModuleUpdate 5 | import com.sanmer.mrepo.model.json.Modules 6 | import retrofit2.Call 7 | import retrofit2.Retrofit 8 | import retrofit2.converter.moshi.MoshiConverterFactory 9 | import retrofit2.create 10 | import retrofit2.http.GET 11 | import retrofit2.http.Path 12 | import timber.log.Timber 13 | 14 | interface ModulesRepoApi { 15 | @GET("json/modules.json") 16 | fun getModules(): Call 17 | 18 | @GET("modules/{id}/update.json") 19 | fun getUpdate(@Path("id") id: String): Call 20 | 21 | companion object { 22 | fun build(repoUrl: String): ModulesRepoApi { 23 | if (BuildConfig.DEBUG) Timber.d("repoUrl: $repoUrl") 24 | 25 | return Retrofit.Builder() 26 | .baseUrl(repoUrl) 27 | .addConverterFactory(MoshiConverterFactory.create()) 28 | .build() 29 | .create() 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/app/Const.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.app 2 | 3 | import android.os.Build 4 | import android.os.Environment 5 | import java.io.File 6 | 7 | object Const { 8 | // DEVICE 9 | val atLeastS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 10 | 11 | // DIR 12 | val DIR_PUBLIC_DOWNLOADS: File = Environment 13 | .getExternalStoragePublicDirectory( 14 | Environment.DIRECTORY_DOWNLOADS 15 | ) 16 | 17 | // NOTIFICATION 18 | const val CHANNEL_ID_DOWNLOAD = "module_download" 19 | 20 | // URL 21 | const val TRANSLATE_URL = "https://weblate.sanmer.dev/engage/mrepo/" 22 | const val ISSUES_URL = "https://github.com/ya0211/MRepo/issues" 23 | const val TELEGRAM_CHANNEL_URL = "https://t.me/mrepo_news" 24 | const val MY_REPO_URL = "https://ya0211.github.io/magisk-modules-repo/" 25 | const val SPDX_URL = "https://spdx.org/licenses/%s.json" 26 | const val UPDATE_URL = "https://ya0211.github.io/mrepo-files/%s.json" 27 | 28 | // CONTEXT 29 | const val KSU_CONTEXT = "u:r:su:s0" 30 | const val MAGISK_CONTEXT = "u:r:magisk:s0" 31 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/app/event/Event.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.app.event 2 | 3 | enum class Event { 4 | NON, 5 | LOADING, 6 | SUCCEEDED, 7 | FAILED 8 | } 9 | 10 | val Event.isNon get() = this == Event.NON 11 | val Event.isLoading get() = this == Event.LOADING 12 | val Event.isSucceeded get() = this == Event.SUCCEEDED 13 | val Event.isFailed get() = this == Event.FAILED 14 | val Event.isFinished get() = isSucceeded || isFailed 15 | val Event.isNotReady get() = isNon || isFailed -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/app/event/State.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.app.event 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlin.reflect.KProperty 8 | 9 | open class State( 10 | initial: Event = Event.NON 11 | ) { 12 | val state = MutableStateFlow(initial) 13 | var event by mutableStateOf(initial) 14 | private set 15 | 16 | val isLoading get() = event.isLoading 17 | val isSucceeded get() = event.isSucceeded 18 | val isFailed get() = event.isFailed 19 | val isFinished get() = event.isFinished 20 | val isNotReady get() = event.isNotReady 21 | 22 | open fun setSucceeded(value: Any? = null) { 23 | event = Event.SUCCEEDED 24 | } 25 | 26 | open fun setFailed(value: Any? = null) { 27 | event = Event.FAILED 28 | } 29 | 30 | open fun setLoading(value: Any? = null) { 31 | event = Event.LOADING 32 | } 33 | 34 | private operator fun MutableState.setValue( 35 | thisObj: Any?, 36 | property: KProperty<*>, 37 | value: Event 38 | ) { 39 | this.value = value 40 | state.value = value 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/app/utils/MediaStoreUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.app.utils 2 | 3 | import android.net.Uri 4 | import android.provider.OpenableColumns 5 | import androidx.core.net.toFile 6 | import androidx.core.net.toUri 7 | import com.sanmer.mrepo.App 8 | import java.io.File 9 | import java.io.InputStream 10 | import java.io.OutputStream 11 | 12 | object MediaStoreUtils { 13 | private val context by lazy { App.context } 14 | private val cr by lazy { context.contentResolver } 15 | 16 | val Uri.displayName: String get() { 17 | if (scheme == "file") { 18 | return toFile().name 19 | } 20 | require(scheme == "content") { "Uri lacks 'content' scheme: $this" } 21 | val projection = arrayOf(OpenableColumns.DISPLAY_NAME) 22 | cr.query(this, projection, null, null, null)?.use { cursor -> 23 | val displayNameColumn = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) 24 | if (cursor.moveToFirst()) { 25 | return cursor.getString(displayNameColumn) 26 | } 27 | } 28 | return this.toString() 29 | } 30 | 31 | fun Uri.copyTo(new: File) { 32 | cr.openInputStream(this)?.use { input -> 33 | cr.openOutputStream(new.toUri())?.use { output -> 34 | input.copyTo(output) 35 | } 36 | } 37 | } 38 | 39 | fun File.newInputStream(): InputStream? = cr.openInputStream(toUri()) 40 | 41 | fun File.newOutputStream(): OutputStream? = cr.openOutputStream(toUri()) 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/app/utils/ShortcutUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.app.utils 2 | 3 | import android.content.Intent 4 | import androidx.core.content.pm.ShortcutInfoCompat 5 | import androidx.core.content.pm.ShortcutManagerCompat 6 | import androidx.core.graphics.drawable.IconCompat 7 | import com.sanmer.mrepo.App 8 | import com.sanmer.mrepo.BuildConfig 9 | import com.sanmer.mrepo.R 10 | import com.sanmer.mrepo.ui.activity.log.LogActivity 11 | 12 | object ShortcutUtils { 13 | private val context by lazy { App.context } 14 | private const val ID_LOGS = "logs" 15 | private const val ID_SETTINGS = "settings" 16 | private const val ID_MODULES = "modules" 17 | const val ACTION_MODULES = "${BuildConfig.APPLICATION_ID}.shortcut.MODULES" 18 | const val ACTION_SETTINGS = "${BuildConfig.APPLICATION_ID}.shortcut.SETTINGS" 19 | 20 | val logs get() = run { 21 | val activity = Intent(Intent.ACTION_MAIN, null, context, LogActivity::class.java) 22 | 23 | ShortcutInfoCompat.Builder(context, ID_LOGS) 24 | .setShortLabel(context.getString(R.string.shortcut_log_label)) 25 | .setLongLabel(context.getString(R.string.shortcut_log_label)) 26 | .setIcon(IconCompat.createWithResource(context, R.drawable.shortcut_log)) 27 | .setIntent(activity) 28 | .build() 29 | } 30 | 31 | val settings get() = run { 32 | val page = Intent(ACTION_SETTINGS) 33 | 34 | ShortcutInfoCompat.Builder(context, ID_SETTINGS) 35 | .setShortLabel(context.getString(R.string.shortcut_settings_label)) 36 | .setLongLabel(context.getString(R.string.shortcut_settings_label)) 37 | .setIcon(IconCompat.createWithResource(context, R.drawable.shortcut_settings)) 38 | .setIntent(page) 39 | .build() 40 | } 41 | 42 | val modules get() = run { 43 | val page = Intent(ACTION_MODULES) 44 | 45 | ShortcutInfoCompat.Builder(context, ID_MODULES) 46 | .setShortLabel(context.getString(R.string.shortcut_modules_label)) 47 | .setLongLabel(context.getString(R.string.shortcut_modules_label)) 48 | .setIcon(IconCompat.createWithResource(context, R.drawable.shortcut_modules)) 49 | .setIntent(page) 50 | .build() 51 | } 52 | 53 | fun push() { 54 | ShortcutManagerCompat.pushDynamicShortcut(context, logs) 55 | ShortcutManagerCompat.pushDynamicShortcut(context, settings) 56 | ShortcutManagerCompat.pushDynamicShortcut(context, modules) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/ModuleDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.sanmer.mrepo.database.dao.ModuleDao 6 | import com.sanmer.mrepo.database.entity.LocalModuleEntity 7 | 8 | @Database(entities = [LocalModuleEntity::class], version = 1) 9 | abstract class ModuleDatabase : RoomDatabase() { 10 | abstract fun moduleDao(): ModuleDao 11 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/RepoDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.migration.Migration 6 | import androidx.sqlite.db.SupportSQLiteDatabase 7 | import com.sanmer.mrepo.database.dao.RepoDao 8 | import com.sanmer.mrepo.database.entity.OnlineModuleEntity 9 | import com.sanmer.mrepo.database.entity.Repo 10 | 11 | @Database(entities = [Repo::class, OnlineModuleEntity::class], version = 2) 12 | abstract class RepoDatabase : RoomDatabase() { 13 | abstract fun repoDao(): RepoDao 14 | 15 | companion object { 16 | val MIGRATION_1_2 = object : Migration(1, 2) { 17 | override fun migrate(database: SupportSQLiteDatabase) { 18 | database.execSQL("CREATE TABLE IF NOT EXISTS repo_new (" + 19 | "url TEXT NOT NULL, " + 20 | "name TEXT NOT NULL, " + 21 | "size INTEGER NOT NULL, " + 22 | "timestamp REAL NOT NULL, " + 23 | "enable INTEGER NOT NULL, " + 24 | "PRIMARY KEY(url))") 25 | database.execSQL("INSERT INTO repo_new (" + 26 | "url, name, size, timestamp, enable) " + 27 | "SELECT " + 28 | "url, name, size, timestamp, enable " + 29 | "FROM repo") 30 | database.execSQL("DROP TABLE repo") 31 | database.execSQL("ALTER TABLE repo_new RENAME TO repo") 32 | 33 | database.execSQL("CREATE TABLE IF NOT EXISTS online_module (" + 34 | "id TEXT NOT NULL, " + 35 | "repo_url TEXT NOT NULL, " + 36 | "name TEXT NOT NULL, " + 37 | "version TEXT NOT NULL, " + 38 | "version_code INTEGER NOT NULL, " + 39 | "author TEXT NOT NULL, " + 40 | "description TEXT NOT NULL, " + 41 | "license TEXT NOT NULL, " + 42 | "zipUrl TEXT NOT NULL, " + 43 | "changelog TEXT NOT NULL, " + 44 | "PRIMARY KEY(id, repo_url))") 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/dao/ModuleDao.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database.dao 2 | 3 | import androidx.room.* 4 | import com.sanmer.mrepo.database.entity.LocalModuleEntity 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | @Dao 8 | interface ModuleDao { 9 | @Query("SELECT * FROM local_module") 10 | fun getLocalAll(): List 11 | 12 | @Query("SELECT * FROM local_module") 13 | fun getLocalAllAsFlow(): Flow> 14 | 15 | @Query("SELECT COUNT(id) FROM local_module") 16 | fun getLocalCount(): Flow 17 | 18 | @Insert(onConflict = OnConflictStrategy.REPLACE) 19 | suspend fun insertLocal(value: LocalModuleEntity) 20 | 21 | @Insert(onConflict = OnConflictStrategy.REPLACE) 22 | suspend fun insertLocal(list: List) 23 | 24 | @Query("DELETE FROM local_module") 25 | suspend fun deleteLocalAll() 26 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/dao/RepoDao.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database.dao 2 | 3 | import androidx.room.* 4 | import com.sanmer.mrepo.database.entity.OnlineModuleEntity 5 | import com.sanmer.mrepo.database.entity.Repo 6 | import com.sanmer.mrepo.database.entity.RepoWithModule 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao 10 | interface RepoDao { 11 | @Transaction 12 | @Query("SELECT * FROM repo") 13 | fun getRepoWithModule(): List 14 | 15 | @Transaction 16 | @Query("SELECT * FROM repo") 17 | fun getRepoWithModuleAsFlow(): Flow> 18 | 19 | @Query("SELECT * FROM repo") 20 | fun getRepoAll(): List 21 | 22 | @Query("SELECT * FROM repo") 23 | fun getRepoAllAsFlow(): Flow> 24 | 25 | @Query("SELECT COUNT(url) FROM repo") 26 | fun getRepoCount(): Flow 27 | 28 | @Query("SELECT COUNT(*) FROM repo WHERE enable LIKE 1") 29 | fun getEnableCount(): Flow 30 | 31 | @Query("SELECT SUM(size) FROM repo WHERE enable LIKE 1") 32 | fun getModuleCount(): Flow 33 | 34 | @Query("SELECT * FROM repo WHERE url LIKE :url LIMIT 1") 35 | fun getRepoByUrl(url: String): Repo 36 | 37 | @Insert(onConflict = OnConflictStrategy.REPLACE) 38 | suspend fun insertRepo(value: Repo) 39 | 40 | @Update(onConflict = OnConflictStrategy.REPLACE) 41 | suspend fun updateRepo(value: Repo) 42 | 43 | @Delete 44 | suspend fun deleteRepo(value: Repo) 45 | 46 | @Query("SELECT * FROM online_module") 47 | fun getModuleAll(): List 48 | 49 | @Insert(onConflict = OnConflictStrategy.REPLACE) 50 | suspend fun insertModule(value: OnlineModuleEntity) 51 | 52 | @Insert(onConflict = OnConflictStrategy.REPLACE) 53 | suspend fun insertModule(list: List) 54 | 55 | @Query("DELETE from online_module where repo_url = :repoUrl") 56 | suspend fun deleteModuleByUrl(repoUrl: String) 57 | 58 | @Query("DELETE FROM online_module") 59 | suspend fun deleteModuleAll() 60 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.sanmer.mrepo.database.ModuleDatabase 6 | import com.sanmer.mrepo.database.RepoDatabase 7 | import com.sanmer.mrepo.database.dao.ModuleDao 8 | import com.sanmer.mrepo.database.dao.RepoDao 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.android.qualifiers.ApplicationContext 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Singleton 15 | 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object DatabaseModule { 20 | 21 | @Provides 22 | @Singleton 23 | fun providesModuleDatabase( 24 | @ApplicationContext context: Context 25 | ): ModuleDatabase { 26 | // MIGRATION 27 | dbRename(context, "mrepo", "module") 28 | 29 | return Room.databaseBuilder(context, 30 | ModuleDatabase::class.java, "module") 31 | .build() 32 | } 33 | 34 | @Provides 35 | @Singleton 36 | fun providesModuleDao(db: ModuleDatabase): ModuleDao = db.moduleDao() 37 | 38 | @Provides 39 | @Singleton 40 | fun providesRepoDatabase( 41 | @ApplicationContext context: Context 42 | ): RepoDatabase { 43 | // MIGRATION 44 | context.filesDir.resolve("repositories").deleteRecursively() 45 | 46 | return Room.databaseBuilder(context, 47 | RepoDatabase::class.java, "repo") 48 | .addMigrations(RepoDatabase.MIGRATION_1_2) 49 | .build() 50 | } 51 | 52 | @Provides 53 | @Singleton 54 | fun providesRepoDao(db: RepoDatabase): RepoDao = db.repoDao() 55 | 56 | @Suppress("SameParameterValue") 57 | private fun dbRename(context: Context, old: String, new: String) { 58 | context.databaseList().forEach { 59 | if (it.startsWith(old)) { 60 | val oldFile = context.getDatabasePath(it) 61 | val newFile = context.getDatabasePath(it.replace(old, new)) 62 | oldFile.renameTo(newFile) 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/entity/LocalModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.sanmer.mrepo.model.module.LocalModule 7 | import com.sanmer.mrepo.model.module.State 8 | 9 | @Entity(tableName = "local_module") 10 | data class LocalModuleEntity( 11 | @PrimaryKey val id: String, 12 | val name: String, 13 | val version: String, 14 | @ColumnInfo(name = "version_code") val versionCode: Int, 15 | val author: String, 16 | val description: String, 17 | val state: Int 18 | ) 19 | 20 | fun LocalModule.toEntity() = LocalModuleEntity( 21 | id = id, 22 | name = name, 23 | version = version, 24 | versionCode = versionCode, 25 | author = author, 26 | description = description, 27 | state = state.toInt() 28 | ) 29 | 30 | fun LocalModuleEntity.toModule() = LocalModule( 31 | id = id, 32 | name = name, 33 | version = version, 34 | versionCode = versionCode, 35 | author = author, 36 | description = description 37 | ).let { 38 | it.state = state.toState() 39 | return@let it 40 | } 41 | 42 | private fun State.toInt() = when (this) { 43 | State.ENABLE -> 0 44 | State.REMOVE -> 1 45 | State.DISABLE -> 2 46 | State.UPDATE -> 3 47 | State.RIRU_DISABLE -> 4 48 | State.ZYGISK_DISABLE -> 5 49 | State.ZYGISK_UNLOADED -> 6 50 | } 51 | 52 | private fun Int.toState() = when (this) { 53 | 0 -> State.ENABLE 54 | 1 -> State.REMOVE 55 | 2 -> State.DISABLE 56 | 3 -> State.UPDATE 57 | 4 -> State.RIRU_DISABLE 58 | 5 -> State.ZYGISK_DISABLE 59 | 6 -> State.ZYGISK_UNLOADED 60 | else -> State.DISABLE 61 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/entity/OnlineModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Embedded 5 | import androidx.room.Entity 6 | import com.sanmer.mrepo.model.module.OnlineModule 7 | import com.sanmer.mrepo.model.module.States 8 | 9 | @Entity(tableName = "online_module", primaryKeys = ["id", "repo_url"]) 10 | data class OnlineModuleEntity( 11 | val id: String, 12 | @ColumnInfo(name = "repo_url") val repoUrl: String, 13 | val name: String, 14 | val version: String, 15 | @ColumnInfo(name = "version_code") val versionCode: Int, 16 | val author: String, 17 | val description: String, 18 | val license: String, 19 | @Embedded val states: StatesEntity 20 | ) 21 | 22 | fun OnlineModuleEntity.toModule() = OnlineModule( 23 | id = id, 24 | name = name, 25 | version = version, 26 | versionCode = versionCode, 27 | author = author, 28 | description = description, 29 | license = license, 30 | states = states.toStates(), 31 | repoUrls = mutableListOf(repoUrl) 32 | ) 33 | 34 | fun OnlineModule.toEntity(repoUrl: String) = OnlineModuleEntity( 35 | id = id, 36 | repoUrl = repoUrl, 37 | name = name, 38 | version = version, 39 | versionCode = versionCode, 40 | author = author, 41 | description = description, 42 | license = license, 43 | states = states.toEntity() 44 | ) 45 | 46 | @Entity(tableName = "states") 47 | data class StatesEntity( 48 | val zipUrl: String, 49 | val changelog: String, 50 | ) 51 | 52 | fun States.toEntity() = StatesEntity( 53 | zipUrl = zipUrl, 54 | changelog = changelog 55 | ) 56 | 57 | fun StatesEntity.toStates() = States( 58 | zipUrl = zipUrl, 59 | changelog = changelog 60 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/database/entity/Repo.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.database.entity 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import androidx.room.Relation 7 | 8 | @Entity(tableName = "repo") 9 | data class Repo( 10 | @PrimaryKey val url: String, 11 | val name: String = url, 12 | val size: Int = 0, 13 | val timestamp: Float = 0f, 14 | var enable: Boolean = true 15 | ) { 16 | override fun equals(other: Any?): Boolean { 17 | return when (other) { 18 | is Repo -> url == other.url 19 | else -> false 20 | } 21 | } 22 | 23 | override fun hashCode(): Int { 24 | return url.hashCode() 25 | } 26 | } 27 | 28 | data class RepoWithModule( 29 | @Embedded val repo: Repo, 30 | @Relation( 31 | parentColumn = "url", 32 | entityColumn = "repo_url", 33 | entity = OnlineModuleEntity::class 34 | ) 35 | val modules: List = listOf() 36 | ) 37 | 38 | fun String.toRepo() = Repo(url = this) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/datastore/UserData.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.datastore 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | import com.sanmer.mrepo.app.Const 6 | import com.sanmer.mrepo.ui.theme.Colors 7 | import com.sanmer.mrepo.utils.expansion.toFile 8 | import java.io.File 9 | 10 | data class UserData( 11 | val workingMode: WorkingMode, 12 | val isRoot: Boolean = workingMode == WorkingMode.MODE_ROOT, 13 | val isNonRoot: Boolean = workingMode == WorkingMode.MODE_NON_ROOT, 14 | val isSetup: Boolean = workingMode == WorkingMode.FIRST_SETUP, 15 | val darkMode: DarkMode, 16 | val themeColor: Int, 17 | val downloadPath: File, 18 | val deleteZipFile: Boolean 19 | ) { 20 | companion object { 21 | fun default() = UserData( 22 | workingMode = WorkingMode.FIRST_SETUP, 23 | darkMode = DarkMode.FOLLOW_SYSTEM, 24 | themeColor = if (Const.atLeastS) Colors.Dynamic.id else Colors.Sakura.id, 25 | downloadPath = Const.DIR_PUBLIC_DOWNLOADS, 26 | deleteZipFile = true 27 | ) 28 | } 29 | } 30 | 31 | @Composable 32 | fun UserData.isDarkMode() = when (darkMode) { 33 | DarkMode.ALWAYS_OFF -> false 34 | DarkMode.ALWAYS_ON -> true 35 | else -> isSystemInDarkTheme() 36 | } 37 | 38 | fun UserData.toPreferences(): UserPreferences = UserPreferences.newBuilder() 39 | .setWorkingMode(workingMode) 40 | .setDarkMode(darkMode) 41 | .setThemeColor(themeColor) 42 | .setDownloadPath(downloadPath.absolutePath) 43 | .setDeleteZipFile(deleteZipFile) 44 | .build() 45 | 46 | fun UserPreferences.toUserData() = UserData( 47 | workingMode = workingMode, 48 | darkMode = darkMode, 49 | themeColor = themeColor, 50 | downloadPath = downloadPath.toFile(), 51 | deleteZipFile = deleteZipFile 52 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/datastore/UserPreferencesDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.datastore 2 | 3 | import androidx.datastore.core.DataStore 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.flow.map 6 | import kotlinx.coroutines.withContext 7 | import javax.inject.Inject 8 | 9 | class UserPreferencesDataSource @Inject constructor( 10 | private val userPreferences: DataStore 11 | ) { 12 | val userData get() = userPreferences.data.map { it.toUserData() } 13 | 14 | suspend fun setWorkingMode(value: WorkingMode) = withContext(Dispatchers.IO) { 15 | userPreferences.updateData { 16 | it.copy { 17 | workingMode = value 18 | } 19 | } 20 | } 21 | 22 | suspend fun setDarkTheme(value: DarkMode) = withContext(Dispatchers.IO) { 23 | userPreferences.updateData { 24 | it.copy { 25 | darkMode = value 26 | } 27 | } 28 | } 29 | 30 | suspend fun setThemeColor(value: Int) = withContext(Dispatchers.IO) { 31 | userPreferences.updateData { 32 | it.copy { 33 | themeColor = value 34 | } 35 | } 36 | } 37 | 38 | suspend fun setDownloadPath(value: String) = withContext(Dispatchers.IO) { 39 | userPreferences.updateData { 40 | it.copy { 41 | downloadPath = value 42 | } 43 | } 44 | } 45 | 46 | suspend fun setDeleteZipFile(value: Boolean) = withContext(Dispatchers.IO) { 47 | userPreferences.updateData { 48 | it.copy { 49 | deleteZipFile = value 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/datastore/UserPreferencesSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.datastore 2 | 3 | import androidx.datastore.core.CorruptionException 4 | import androidx.datastore.core.Serializer 5 | import com.google.protobuf.InvalidProtocolBufferException 6 | import java.io.InputStream 7 | import java.io.OutputStream 8 | import javax.inject.Inject 9 | 10 | 11 | class UserPreferencesSerializer @Inject constructor() : Serializer { 12 | override val defaultValue: UserPreferences = UserData.default().toPreferences() 13 | 14 | override suspend fun readFrom(input: InputStream): UserPreferences = 15 | try { 16 | UserPreferences.parseFrom(input) 17 | } catch (exception: InvalidProtocolBufferException) { 18 | throw CorruptionException("cannot read proto", exception) 19 | } 20 | 21 | override suspend fun writeTo(t: UserPreferences, output: OutputStream) { 22 | t.writeTo(output) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/datastore/di/DataStoreModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.datastore.di 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.core.DataStoreFactory 6 | import androidx.datastore.dataStoreFile 7 | import com.sanmer.mrepo.datastore.UserPreferences 8 | import com.sanmer.mrepo.datastore.UserPreferencesSerializer 9 | import com.sanmer.mrepo.di.ApplicationScope 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.android.qualifiers.ApplicationContext 14 | import dagger.hilt.components.SingletonComponent 15 | import kotlinx.coroutines.CoroutineScope 16 | import javax.inject.Singleton 17 | 18 | @Module 19 | @InstallIn(SingletonComponent::class) 20 | object DataStoreModule { 21 | 22 | @Provides 23 | @Singleton 24 | fun providesUserPreferencesDataStore( 25 | @ApplicationContext context: Context, 26 | userPreferencesSerializer: UserPreferencesSerializer, 27 | @ApplicationScope applicationScope: CoroutineScope 28 | ): DataStore = 29 | DataStoreFactory.create( 30 | serializer = userPreferencesSerializer, 31 | scope = applicationScope, 32 | ) { 33 | context.dataStoreFile("user_preferences.pb") 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/di/CoroutineDispatcherModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | object CoroutineDispatcherModule { 13 | 14 | @DefaultDispatcher 15 | @Provides 16 | fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 17 | 18 | @IoDispatcher 19 | @Provides 20 | fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO 21 | 22 | @MainDispatcher 23 | @Provides 24 | fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main 25 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/di/CoroutineQualifier.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Retention(AnnotationRetention.RUNTIME) 6 | @Qualifier 7 | annotation class DefaultDispatcher 8 | 9 | @Retention(AnnotationRetention.RUNTIME) 10 | @Qualifier 11 | annotation class IoDispatcher 12 | 13 | @Retention(AnnotationRetention.RUNTIME) 14 | @Qualifier 15 | annotation class MainDispatcher 16 | 17 | @Retention(AnnotationRetention.RUNTIME) 18 | @Qualifier 19 | annotation class ApplicationScope 20 | 21 | @Retention(AnnotationRetention.RUNTIME) 22 | @Qualifier 23 | annotation class MainScope 24 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/di/CoroutineScopeModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.SupervisorJob 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object CoroutineScopeModule { 15 | 16 | @ApplicationScope 17 | @Provides 18 | @Singleton 19 | fun providesDefaultCoroutineScope( 20 | @DefaultDispatcher defaultDispatcher: CoroutineDispatcher 21 | ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher) 22 | 23 | @MainScope 24 | @Provides 25 | @Singleton 26 | fun providesMainCoroutineScope( 27 | @MainDispatcher mainDispatcher: CoroutineDispatcher 28 | ): CoroutineScope = CoroutineScope(SupervisorJob() + mainDispatcher) 29 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/model/json/AppUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.model.json 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class AppUpdate( 7 | val version: String, 8 | val versionCode: Int, 9 | val apkUrl: String, 10 | val changelog: String 11 | ) { 12 | companion object { 13 | fun empty() = AppUpdate( 14 | version = "1", 15 | versionCode = 1, 16 | apkUrl = "", 17 | changelog = "" 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/model/json/License.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.model.json 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class License( 7 | val licenseText: String, 8 | val name: String, 9 | val licenseId: String, 10 | val seeAlso: List, 11 | val isOsiApproved: Boolean, 12 | val isFsfLibre: Boolean = false, 13 | ) { 14 | fun hasLabel() = isFsfLibre || isOsiApproved 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/model/json/ModuleUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.model.json 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class ModuleUpdate( 8 | val timestamp: Float, 9 | val versions: List, 10 | @Json(ignore = true) val repoUrl: String = "" 11 | ) 12 | 13 | @JsonClass(generateAdapter = true) 14 | data class ModuleUpdateItem( 15 | val timestamp: Float, 16 | val version: String, 17 | val versionCode: Int, 18 | val zipUrl: String, 19 | val changelog: String, 20 | @Json(ignore = true) val repoUrl: String = "" 21 | ) 22 | 23 | val ModuleUpdateItem.versionDisplay get() = if ("(${versionCode})" in version) { 24 | version 25 | } else { 26 | "$version (${versionCode})" 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/model/json/Modules.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.model.json 2 | 3 | import com.sanmer.mrepo.model.module.OnlineModule 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | 7 | @JsonClass(generateAdapter = true) 8 | data class Modules( 9 | val name: String, 10 | val timestamp: Float, 11 | val modules: List, 12 | @Json(ignore = true) val url: String = "" 13 | ) { 14 | override fun equals(other: Any?): Boolean { 15 | return when (other) { 16 | is Modules -> url == other.url 17 | else -> false 18 | } 19 | } 20 | 21 | override fun hashCode(): Int { 22 | return url.hashCode() 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/model/module/LocalModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.model.module 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | 7 | data class LocalModule( 8 | var id: String = "unknown", 9 | var name: String = id, 10 | var version: String = id, 11 | var versionCode: Int = -1, 12 | var author: String = id, 13 | var description: String = id 14 | ) { 15 | var state by mutableStateOf(State.DISABLE) 16 | 17 | val versionDisplay get() = if ("(${versionCode})" in version) { 18 | version 19 | } else { 20 | "$version (${versionCode})" 21 | } 22 | 23 | override fun equals(other: Any?): Boolean { 24 | return when (other) { 25 | is LocalModule -> id == other.id 26 | is OnlineModule -> id == other.id 27 | else -> false 28 | } 29 | } 30 | 31 | override fun hashCode(): Int { 32 | return id.hashCode() 33 | } 34 | } 35 | 36 | enum class State { 37 | ENABLE, 38 | REMOVE, 39 | DISABLE, 40 | UPDATE, 41 | RIRU_DISABLE, 42 | ZYGISK_DISABLE, 43 | ZYGISK_UNLOADED 44 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/model/module/OnlineModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.model.module 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class OnlineModule( 8 | val id: String = "unknown", 9 | val name: String = id, 10 | val version: String = id, 11 | val versionCode: Int = -1, 12 | val author: String = id, 13 | val description: String = id, 14 | val license: String = id, 15 | val states: States = States(), 16 | @Json(ignore = true) val repoUrls: MutableList = mutableListOf() 17 | ) { 18 | val repoUrl get() = try { 19 | repoUrls.first() 20 | } catch (e: NoSuchElementException) { 21 | "" 22 | } 23 | 24 | val versionDisplay get() = if ("(${versionCode})" in version) { 25 | version 26 | } else { 27 | "$version (${versionCode})" 28 | } 29 | 30 | override fun equals(other: Any?): Boolean { 31 | return when (other) { 32 | is LocalModule -> id == other.id 33 | is OnlineModule -> id == other.id 34 | else -> false 35 | } 36 | } 37 | 38 | override fun hashCode(): Int { 39 | return id.hashCode() 40 | } 41 | } 42 | 43 | @JsonClass(generateAdapter = true) 44 | data class States( 45 | val zipUrl: String = "", 46 | val changelog: String = "", 47 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/provider/SuProvider.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.provider 2 | 3 | import com.sanmer.mrepo.api.local.ModulesLocalApi 4 | import com.sanmer.mrepo.app.event.Event 5 | import com.topjohnwu.superuser.nio.FileSystemManager 6 | import kotlinx.coroutines.flow.StateFlow 7 | 8 | interface SuProvider { 9 | val state: StateFlow 10 | 11 | val pid: Int 12 | 13 | val context: String 14 | 15 | val enforce: Int 16 | 17 | fun getFileSystemManager(): FileSystemManager 18 | 19 | fun getModulesApi(): ModulesLocalApi 20 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/provider/di/ProviderModule.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.provider.di 2 | 3 | import com.sanmer.mrepo.provider.SuProvider 4 | import com.sanmer.mrepo.provider.SuProviderImpl 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | abstract class ProviderModule { 14 | 15 | @Binds 16 | @Singleton 17 | abstract fun bindsSuProvider(suProviderImpl: SuProviderImpl): SuProvider 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/repository/SuRepository.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.repository 2 | 3 | import com.sanmer.mrepo.api.local.ModulesLocalApi 4 | import com.sanmer.mrepo.app.event.Event 5 | import com.sanmer.mrepo.model.module.LocalModule 6 | import com.sanmer.mrepo.provider.SuProvider 7 | import com.topjohnwu.superuser.nio.FileSystemManager 8 | import kotlinx.coroutines.flow.StateFlow 9 | import java.io.File 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | 13 | @Singleton 14 | class SuRepository @Inject constructor( 15 | private val suProvider: SuProvider 16 | ) { 17 | private val api: ModulesLocalApi get() = suProvider.getModulesApi() 18 | 19 | val state: StateFlow get() = suProvider.state 20 | val pid get() = suProvider.pid 21 | val context get() = suProvider.context 22 | val enforce get() = suProvider.enforce 23 | val fs: FileSystemManager get() = suProvider.getFileSystemManager() 24 | 25 | val version get() = api.version 26 | suspend fun getModules(): Result> = api.getModules() 27 | fun enable(module: LocalModule) = api.enable(module) 28 | fun disable(module: LocalModule) = api.disable(module) 29 | fun remove(module: LocalModule) = api.remove(module) 30 | fun install( 31 | console: (String) -> Unit, 32 | onSuccess: (LocalModule) -> Unit, 33 | onFailure: () -> Unit, 34 | zipFile: File 35 | ) = api.install(console, onSuccess, onFailure, zipFile) 36 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/repository/UserDataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.repository 2 | 3 | import com.sanmer.mrepo.datastore.DarkMode 4 | import com.sanmer.mrepo.datastore.UserData 5 | import com.sanmer.mrepo.datastore.UserPreferencesDataSource 6 | import com.sanmer.mrepo.datastore.WorkingMode 7 | import com.sanmer.mrepo.di.ApplicationScope 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.flow.distinctUntilChanged 10 | import kotlinx.coroutines.flow.launchIn 11 | import kotlinx.coroutines.flow.onEach 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | import javax.inject.Singleton 15 | 16 | @Singleton 17 | class UserDataRepository @Inject constructor( 18 | private val userPreferencesDataSource: UserPreferencesDataSource, 19 | @ApplicationScope private val applicationScope: CoroutineScope 20 | ) { 21 | val userData get() = userPreferencesDataSource.userData 22 | 23 | private val default = UserData.default() 24 | private var _downloadPath = default.downloadPath 25 | private var _deleteZipFile = default.deleteZipFile 26 | val downloadPath get() = _downloadPath 27 | val deleteZipFile get() = _deleteZipFile 28 | 29 | init { 30 | userPreferencesDataSource.userData 31 | .distinctUntilChanged() 32 | .onEach { 33 | _downloadPath = it.downloadPath 34 | _deleteZipFile = it.deleteZipFile 35 | }.launchIn(applicationScope) 36 | } 37 | 38 | fun setWorkingMode(value: WorkingMode) = applicationScope.launch { 39 | userPreferencesDataSource.setWorkingMode(value) 40 | } 41 | 42 | fun setDarkTheme(value: DarkMode) = applicationScope.launch { 43 | userPreferencesDataSource.setDarkTheme(value) 44 | } 45 | 46 | fun setThemeColor(value: Int) = applicationScope.launch { 47 | userPreferencesDataSource.setThemeColor(value) 48 | } 49 | 50 | fun setDownloadPath(value: String) = applicationScope.launch { 51 | userPreferencesDataSource.setDownloadPath(value) 52 | } 53 | 54 | fun setDeleteZipFile(value: Boolean) = applicationScope.launch { 55 | userPreferencesDataSource.setDeleteZipFile(value) 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/service/LogcatService.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.service 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.compose.runtime.mutableStateListOf 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleService 8 | import androidx.lifecycle.lifecycleScope 9 | import androidx.lifecycle.repeatOnLifecycle 10 | import com.sanmer.mrepo.utils.log.LogText 11 | import com.sanmer.mrepo.utils.log.Logcat 12 | import com.sanmer.mrepo.utils.log.Logcat.toLogTextList 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.isActive 17 | import kotlinx.coroutines.launch 18 | 19 | class LogcatService : LifecycleService() { 20 | override fun onCreate() { 21 | super.onCreate() 22 | isActive.value = true 23 | } 24 | 25 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 26 | lifecycleScope.launch(Dispatchers.Default) { 27 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 28 | val old = Logcat.readLogs() 29 | console.addAll( 30 | old.filter { it !in console } 31 | ) 32 | 33 | while (isActive) { 34 | val logs = Logcat.getCurrent().toLogTextList() 35 | val new = logs.filter { it !in console } 36 | console.addAll(new) 37 | new.forEach { 38 | Logcat.writeLog(it) 39 | } 40 | 41 | delay(1000) 42 | } 43 | } 44 | } 45 | 46 | return super.onStartCommand(intent, flags, startId) 47 | } 48 | 49 | override fun onDestroy() { 50 | super.onDestroy() 51 | isActive.value = false 52 | } 53 | 54 | companion object { 55 | val console = mutableStateListOf() 56 | val isActive = MutableStateFlow(false) 57 | 58 | fun start( 59 | context: Context, 60 | ) { 61 | val intent = Intent(context, LogcatService::class.java) 62 | context.startService(intent) 63 | } 64 | 65 | fun stop( 66 | context: Context, 67 | ) { 68 | val intent = Intent(context, LogcatService::class.java) 69 | context.stopService(intent) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/activity/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.activity.base 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.getValue 8 | import androidx.core.view.WindowCompat 9 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 10 | import com.sanmer.mrepo.datastore.UserData 11 | import com.sanmer.mrepo.datastore.isDarkMode 12 | import com.sanmer.mrepo.repository.UserDataRepository 13 | import com.sanmer.mrepo.ui.theme.AppTheme 14 | import dagger.hilt.android.AndroidEntryPoint 15 | import javax.inject.Inject 16 | 17 | @AndroidEntryPoint 18 | abstract class BaseActivity : ComponentActivity() { 19 | @Inject 20 | lateinit var userDataRepository: UserDataRepository 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | WindowCompat.setDecorFitsSystemWindows(window, false) 25 | } 26 | 27 | fun setActivityContent( 28 | content: @Composable () -> Unit 29 | ) = setContent { 30 | val userData by userDataRepository.userData 31 | .collectAsStateWithLifecycle(UserData.default()) 32 | 33 | AppTheme( 34 | darkMode = userData.isDarkMode(), 35 | themeColor = userData.themeColor 36 | ) { 37 | content() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/activity/install/InstallActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.activity.install 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import androidx.activity.viewModels 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.core.net.toUri 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner 12 | import com.sanmer.mrepo.app.event.isSucceeded 13 | import com.sanmer.mrepo.ui.activity.base.BaseActivity 14 | import com.sanmer.mrepo.viewmodel.InstallViewModel 15 | import kotlinx.coroutines.flow.launchIn 16 | import kotlinx.coroutines.flow.onEach 17 | import timber.log.Timber 18 | import java.io.File 19 | 20 | class InstallActivity : BaseActivity() { 21 | private val viewModel: InstallViewModel by viewModels() 22 | 23 | init { 24 | Timber.d("InstallActivity init") 25 | } 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | 30 | cacheDir.resolve("log") 31 | .walkBottomUp() 32 | .forEach { 33 | if (it.name.startsWith("module")) { 34 | it.delete() 35 | } 36 | } 37 | 38 | viewModel.suState.onEach { 39 | if (it.isSucceeded) { 40 | val uri = intent.data 41 | if (uri != null) { 42 | viewModel.install(this, uri) 43 | } else { 44 | viewModel.state.setFailed("The uri is null!") 45 | } 46 | } 47 | }.launchIn(lifecycleScope) 48 | 49 | setActivityContent { 50 | CompositionLocalProvider( 51 | LocalViewModelStoreOwner provides this 52 | ) { 53 | InstallScreen() 54 | } 55 | } 56 | } 57 | 58 | companion object { 59 | fun start(context: Context, uri: Uri) { 60 | val intent = Intent(context, InstallActivity::class.java).apply { 61 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 62 | data = uri 63 | } 64 | context.startActivity(intent) 65 | } 66 | 67 | fun start(context: Context, path: File) = start(context, path.toUri()) 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/activity/license/LicenseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.activity.license 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.sanmer.mrepo.ui.activity.base.BaseActivity 7 | 8 | class LicenseActivity : BaseActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | val licenseId = intent.getStringExtra(LICENSE_ID) ?: "UNKNOWN" 13 | 14 | setActivityContent { 15 | LicenseScreen( 16 | licenseId = licenseId 17 | ) 18 | } 19 | } 20 | 21 | companion object { 22 | private const val LICENSE_ID = "LICENSE_ID" 23 | 24 | fun start( 25 | context: Context, 26 | licenseId: String 27 | ) { 28 | val intent = Intent(context, LicenseActivity::class.java) 29 | intent.putExtra(LICENSE_ID, licenseId) 30 | context.startActivity(intent) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/activity/log/LogActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.activity.log 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.lifecycleScope 8 | import androidx.lifecycle.repeatOnLifecycle 9 | import com.sanmer.mrepo.service.LogcatService 10 | import com.sanmer.mrepo.ui.activity.base.BaseActivity 11 | import kotlinx.coroutines.launch 12 | 13 | class LogActivity : BaseActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | lifecycleScope.launch { 18 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 19 | LogcatService.isActive 20 | .collect { isActive -> 21 | if (!isActive) { 22 | LogcatService.start(this@LogActivity) 23 | } 24 | } 25 | } 26 | } 27 | 28 | setActivityContent { 29 | LogScreen() 30 | } 31 | } 32 | 33 | override fun onDestroy() { 34 | super.onDestroy() 35 | LogcatService.stop(this) 36 | } 37 | 38 | companion object { 39 | fun start(context: Context) { 40 | val intent = Intent(context, LogActivity::class.java) 41 | context.startActivity(intent) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/activity/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.activity.main 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.view.ViewTreeObserver 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.lifecycleScope 9 | import androidx.lifecycle.repeatOnLifecycle 10 | import com.sanmer.mrepo.datastore.WorkingMode 11 | import com.sanmer.mrepo.ui.activity.base.BaseActivity 12 | import com.sanmer.mrepo.app.utils.NotificationUtils 13 | import kotlinx.coroutines.flow.distinctUntilChanged 14 | import kotlinx.coroutines.launch 15 | 16 | class MainActivity : BaseActivity() { 17 | private var isReady = false 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | 22 | val content: View = findViewById(android.R.id.content) 23 | content.viewTreeObserver.addOnPreDrawListener( 24 | object : ViewTreeObserver.OnPreDrawListener { 25 | override fun onPreDraw(): Boolean = if (isReady) { 26 | content.viewTreeObserver.removeOnPreDrawListener(this) 27 | true 28 | } else { 29 | false 30 | } 31 | } 32 | ) 33 | 34 | lifecycleScope.launch { 35 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 36 | userDataRepository.userData 37 | .distinctUntilChanged() 38 | .collect { 39 | if (it.isSetup) { 40 | setSetup() 41 | } else { 42 | setMain() 43 | } 44 | isReady = true 45 | } 46 | } 47 | } 48 | } 49 | 50 | private fun setSetup() = setActivityContent { 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 52 | NotificationUtils.PermissionState() 53 | } 54 | 55 | SetupScreen( 56 | onRoot = { 57 | userDataRepository.setWorkingMode(WorkingMode.MODE_ROOT) 58 | }, 59 | onNonRoot = { 60 | userDataRepository.setWorkingMode(WorkingMode.MODE_NON_ROOT) 61 | } 62 | ) 63 | } 64 | 65 | private fun setMain() = setActivityContent { 66 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 67 | NotificationUtils.PermissionState() 68 | } 69 | 70 | MainScreen() 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/activity/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.activity.main 2 | 3 | import androidx.compose.animation.core.tween 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.foundation.layout.WindowInsets 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.safeContent 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.platform.LocalContext 13 | import com.google.accompanist.navigation.animation.AnimatedNavHost 14 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController 15 | import com.sanmer.mrepo.app.utils.ShortcutUtils 16 | import com.sanmer.mrepo.ui.navigation.BottomNav 17 | import com.sanmer.mrepo.ui.navigation.MainGraph 18 | import com.sanmer.mrepo.ui.navigation.graph.homeGraph 19 | import com.sanmer.mrepo.ui.navigation.graph.modulesGraph 20 | import com.sanmer.mrepo.ui.navigation.graph.settingsGraph 21 | 22 | @Composable 23 | fun MainScreen() { 24 | val navController = rememberAnimatedNavController() 25 | val that = LocalContext.current as MainActivity 26 | 27 | val startDestination = when (that.intent.action) { 28 | ShortcutUtils.ACTION_MODULES -> MainGraph.Modules.route 29 | ShortcutUtils.ACTION_SETTINGS -> MainGraph.Settings.route 30 | else -> MainGraph.Home.route 31 | } 32 | 33 | Scaffold( 34 | bottomBar = { 35 | BottomNav(navController = navController) 36 | }, 37 | contentWindowInsets = WindowInsets.safeContent 38 | ) { 39 | AnimatedNavHost( 40 | modifier = Modifier 41 | .padding(bottom = it.calculateBottomPadding()), 42 | navController = navController, 43 | startDestination = startDestination, 44 | enterTransition = { fadeIn(animationSpec = tween(400)) }, 45 | exitTransition = { fadeOut(animationSpec = tween(300)) } 46 | ) { 47 | homeGraph( 48 | //navController = navController 49 | ) 50 | modulesGraph( 51 | navController = navController 52 | ) 53 | settingsGraph( 54 | navController = navController 55 | ) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/animate/EnterTransition.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.animate 2 | 3 | import androidx.compose.animation.core.FiniteAnimationSpec 4 | import androidx.compose.animation.core.Spring 5 | import androidx.compose.animation.core.VisibilityThreshold 6 | import androidx.compose.animation.core.spring 7 | import androidx.compose.animation.slideIn 8 | import androidx.compose.ui.unit.IntOffset 9 | 10 | fun slideInBottomToTop( 11 | animationSpec: FiniteAnimationSpec = 12 | spring( 13 | stiffness = Spring.StiffnessMediumLow, 14 | visibilityThreshold = IntOffset.VisibilityThreshold 15 | ) 16 | ) = slideIn( 17 | initialOffset = { IntOffset(0, it.height) }, 18 | animationSpec = animationSpec 19 | ) 20 | 21 | fun slideInTopToBottom( 22 | animationSpec: FiniteAnimationSpec = 23 | spring( 24 | stiffness = Spring.StiffnessMediumLow, 25 | visibilityThreshold = IntOffset.VisibilityThreshold 26 | ) 27 | ) = slideIn( 28 | initialOffset = { IntOffset(0, - it.height) }, 29 | animationSpec = animationSpec 30 | ) 31 | 32 | fun slideInLeftToRight( 33 | animationSpec: FiniteAnimationSpec = 34 | spring( 35 | stiffness = Spring.StiffnessMediumLow, 36 | visibilityThreshold = IntOffset.VisibilityThreshold 37 | ) 38 | ) = slideIn( 39 | initialOffset = { IntOffset(- it.width, 0) }, 40 | animationSpec = animationSpec 41 | ) 42 | 43 | fun slideInRightToLeft( 44 | animationSpec: FiniteAnimationSpec = 45 | spring( 46 | stiffness = Spring.StiffnessMediumLow, 47 | visibilityThreshold = IntOffset.VisibilityThreshold 48 | ) 49 | ) = slideIn( 50 | initialOffset = { IntOffset(it.width, 0) }, 51 | animationSpec = animationSpec 52 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/animate/ExitTransition.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.animate 2 | 3 | import androidx.compose.animation.core.FiniteAnimationSpec 4 | import androidx.compose.animation.core.Spring 5 | import androidx.compose.animation.core.VisibilityThreshold 6 | import androidx.compose.animation.core.spring 7 | import androidx.compose.animation.slideOut 8 | import androidx.compose.ui.unit.IntOffset 9 | 10 | fun slideOutTopToBottom( 11 | animationSpec: FiniteAnimationSpec = 12 | spring( 13 | stiffness = Spring.StiffnessMediumLow, 14 | visibilityThreshold = IntOffset.VisibilityThreshold 15 | ) 16 | ) = slideOut( 17 | targetOffset = { IntOffset(0, it.height) }, 18 | animationSpec = animationSpec 19 | ) 20 | 21 | fun slideOutBottomToTop( 22 | animationSpec: FiniteAnimationSpec = 23 | spring( 24 | stiffness = Spring.StiffnessMediumLow, 25 | visibilityThreshold = IntOffset.VisibilityThreshold 26 | ) 27 | ) = slideOut( 28 | targetOffset = { IntOffset(0, - it.height) }, 29 | animationSpec = animationSpec 30 | ) 31 | 32 | fun slideOutRightToLeft( 33 | animationSpec: FiniteAnimationSpec = 34 | spring( 35 | stiffness = Spring.StiffnessMediumLow, 36 | visibilityThreshold = IntOffset.VisibilityThreshold 37 | ) 38 | ) = slideOut( 39 | targetOffset = { IntOffset(- it.width, 0) }, 40 | animationSpec = animationSpec 41 | ) 42 | 43 | fun slideOutLeftToRight( 44 | animationSpec: FiniteAnimationSpec = 45 | spring( 46 | stiffness = Spring.StiffnessMediumLow, 47 | visibilityThreshold = IntOffset.VisibilityThreshold 48 | ) 49 | ) = slideOut( 50 | targetOffset = { IntOffset(it.width, 0) }, 51 | animationSpec = animationSpec 52 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/component/AppBar.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.component 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.FastOutLinearInEasing 5 | import androidx.compose.animation.core.Spring 6 | import androidx.compose.animation.core.spring 7 | import androidx.compose.material3.ColorScheme 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.TopAppBarScrollBehavior 10 | import androidx.compose.material3.surfaceColorAtElevation 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.lerp 15 | import androidx.compose.ui.unit.Dp 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun AppBarContainerColor( 20 | scrollBehavior: TopAppBarScrollBehavior, 21 | content: @Composable (Color) -> Unit, 22 | ) { 23 | val colorTransitionFraction = scrollBehavior.state.overlappedFraction 24 | val fraction = if (colorTransitionFraction > 0.01f) 1f else 0f 25 | val containerColor by animateColorAsState( 26 | targetValue = containerColor(fraction), 27 | animationSpec = spring(stiffness = Spring.StiffnessMediumLow) 28 | ) 29 | 30 | content(containerColor) 31 | } 32 | 33 | @Composable 34 | private fun containerColor(colorTransitionFraction: Float): Color { 35 | val containerColor = MaterialTheme.colorScheme.surface 36 | val scrolledContainerColor = MaterialTheme.colorScheme.applyTonalElevation( 37 | backgroundColor = containerColor, 38 | elevation = 3.0.dp 39 | ) 40 | 41 | return lerp( 42 | containerColor, 43 | scrolledContainerColor, 44 | FastOutLinearInEasing.transform(colorTransitionFraction) 45 | ) 46 | } 47 | 48 | private fun ColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color { 49 | return if (backgroundColor == surface) { 50 | surfaceColorAtElevation(elevation) 51 | } else { 52 | backgroundColor 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/component/DropdownMenu.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.component 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.DropdownMenu 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.DpOffset 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.window.PopupProperties 14 | 15 | @Composable 16 | fun DropdownMenu( 17 | expanded: Boolean, 18 | onDismissRequest: () -> Unit, 19 | modifier: Modifier = Modifier, 20 | shape: RoundedCornerShape = RoundedCornerShape(15.dp), 21 | offset: DpOffset = DpOffset(0.dp, 0.dp), 22 | properties: PopupProperties = PopupProperties(focusable = true), 23 | content: @Composable ColumnScope.() -> Unit 24 | ) = CustomMenuShape(shape) { 25 | DropdownMenu( 26 | expanded = expanded, 27 | onDismissRequest = onDismissRequest, 28 | modifier = modifier, 29 | offset = offset, 30 | properties = properties, 31 | content = content 32 | ) 33 | } 34 | 35 | @Composable 36 | fun DropdownMenu( 37 | expanded: Boolean, 38 | onDismissRequest: () -> Unit, 39 | modifier: Modifier = Modifier, 40 | shape: RoundedCornerShape = RoundedCornerShape(15.dp), 41 | contentAlignment: Alignment = Alignment.TopStart, 42 | offset: DpOffset = DpOffset(0.dp, 0.dp), 43 | properties: PopupProperties = PopupProperties(focusable = true), 44 | surface: @Composable () -> Unit, 45 | content: @Composable ColumnScope.() -> Unit 46 | ) = Box { 47 | surface() 48 | CustomMenuShape(shape) { 49 | Box( 50 | modifier = Modifier 51 | .align(contentAlignment), 52 | contentAlignment = contentAlignment 53 | ) { 54 | DropdownMenu( 55 | expanded = expanded, 56 | onDismissRequest = onDismissRequest, 57 | modifier = modifier, 58 | offset = offset, 59 | properties = properties, 60 | content = content 61 | ) 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | private fun CustomMenuShape( 68 | shape: RoundedCornerShape, 69 | content: @Composable () -> Unit 70 | ) = MaterialTheme( 71 | shapes = MaterialTheme.shapes.copy(extraSmall = shape), 72 | content = content 73 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/component/PageIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.component 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.painterResource 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.text.font.FontFamily 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.text.style.TextAlign 17 | import androidx.compose.ui.text.style.TextOverflow 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | 21 | @Composable 22 | fun PageIndicator( 23 | icon: @Composable ColumnScope.() -> Unit, 24 | text: @Composable ColumnScope.() -> Unit, 25 | modifier: Modifier = Modifier 26 | ) { 27 | Column( 28 | modifier = modifier 29 | .fillMaxSize(), 30 | horizontalAlignment = Alignment.CenterHorizontally, 31 | verticalArrangement = Arrangement.Center 32 | ) { 33 | icon() 34 | Spacer(modifier = Modifier.height(20.dp)) 35 | text() 36 | } 37 | } 38 | 39 | @Composable 40 | fun PageIndicator( 41 | @DrawableRes icon: Int, 42 | text: String, 43 | modifier: Modifier = Modifier, 44 | ) = PageIndicator( 45 | modifier = modifier, 46 | icon = { 47 | Icon( 48 | painter = painterResource(id = icon), 49 | contentDescription = null, 50 | tint = MaterialTheme.colorScheme.outline.copy(0.2f), 51 | modifier = Modifier 52 | .size(80.dp) 53 | ) 54 | }, 55 | text = { 56 | Text( 57 | text = text, 58 | color = MaterialTheme.colorScheme.outline.copy(0.5f), 59 | fontSize = 20.sp, 60 | fontFamily = FontFamily.SansSerif, 61 | fontWeight = FontWeight.Medium, 62 | textAlign = TextAlign.Center, 63 | modifier = Modifier.padding(horizontal = 20.dp), 64 | maxLines = 5, 65 | overflow = TextOverflow.Ellipsis, 66 | ) 67 | } 68 | ) 69 | 70 | @Composable 71 | fun PageIndicator( 72 | @DrawableRes icon: Int, 73 | @StringRes text: Int, 74 | modifier: Modifier = Modifier 75 | ) = PageIndicator( 76 | modifier = modifier, 77 | icon = icon, 78 | text = stringResource(id = text) 79 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/navigation/Main.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.navigation 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import androidx.navigation.NavController 6 | import com.sanmer.mrepo.R 7 | import com.sanmer.mrepo.ui.utils.navigatePopUpTo 8 | 9 | sealed class MainGraph( 10 | val route: String, 11 | @StringRes val label: Int, 12 | @DrawableRes val icon: Int, 13 | @DrawableRes val iconSelected: Int 14 | ) { 15 | object Home : MainGraph( 16 | route = "homeGraph", 17 | label = R.string.page_home, 18 | icon = R.drawable.home_outline, 19 | iconSelected = R.drawable.home_bold 20 | ) 21 | object Modules : MainGraph( 22 | route = "modulesGraph", 23 | label = R.string.page_modules, 24 | icon = R.drawable.box_outline, 25 | iconSelected = R.drawable.box_bold 26 | ) 27 | object Settings : MainGraph( 28 | route = "settingsGraph", 29 | label = R.string.page_settings, 30 | icon = R.drawable.setting_outline, 31 | iconSelected = R.drawable.setting_bold 32 | ) 33 | } 34 | 35 | fun NavController.navigateToHome() = navigatePopUpTo(MainGraph.Home.route) 36 | fun NavController.navigateToModules() = navigatePopUpTo(MainGraph.Modules.route) 37 | fun NavController.navigateToSettings() = navigatePopUpTo(MainGraph.Settings.route) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/navigation/graph/Home.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.navigation.graph 2 | 3 | import androidx.navigation.NavGraphBuilder 4 | import com.google.accompanist.navigation.animation.composable 5 | import com.google.accompanist.navigation.animation.navigation 6 | import com.sanmer.mrepo.ui.navigation.MainGraph 7 | import com.sanmer.mrepo.ui.screens.home.HomeScreen 8 | 9 | sealed class HomeGraph(val route: String) { 10 | object Home : HomeGraph("home") 11 | } 12 | 13 | fun NavGraphBuilder.homeGraph( 14 | //navController: NavController 15 | ) = navigation( 16 | startDestination = HomeGraph.Home.route, 17 | route = MainGraph.Home.route 18 | ) { 19 | composable(HomeGraph.Home.route) { 20 | HomeScreen() 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/navigation/graph/Modules.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.navigation.graph 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavGraphBuilder 5 | import androidx.navigation.NavType 6 | import androidx.navigation.navArgument 7 | import com.google.accompanist.navigation.animation.composable 8 | import com.google.accompanist.navigation.animation.navigation 9 | import com.sanmer.mrepo.ui.animate.slideInLeftToRight 10 | import com.sanmer.mrepo.ui.animate.slideInRightToLeft 11 | import com.sanmer.mrepo.ui.animate.slideOutLeftToRight 12 | import com.sanmer.mrepo.ui.animate.slideOutRightToLeft 13 | import com.sanmer.mrepo.ui.navigation.MainGraph 14 | import com.sanmer.mrepo.ui.screens.modules.ModulesScreen 15 | import com.sanmer.mrepo.ui.screens.viewmodule.ViewModuleScreen 16 | 17 | sealed class ModulesGraph(val route: String) { 18 | object Modules : ModulesGraph("modules") 19 | object View : ModulesGraph("view") { 20 | val way: String = "${route}/{id}" 21 | fun String.toRoute() = "${route}/${this}" 22 | } 23 | } 24 | 25 | fun NavGraphBuilder.modulesGraph( 26 | navController: NavController 27 | ) = navigation( 28 | startDestination = ModulesGraph.Modules.route, 29 | route = MainGraph.Modules.route 30 | ) { 31 | composable( 32 | route = ModulesGraph.Modules.route, 33 | enterTransition = { 34 | when (initialState.destination.route) { 35 | ModulesGraph.View.way -> slideInRightToLeft() 36 | else -> null 37 | } 38 | }, 39 | exitTransition = { 40 | when (initialState.destination.route) { 41 | ModulesGraph.View.way -> slideOutLeftToRight() 42 | else -> null 43 | } 44 | } 45 | ) { 46 | ModulesScreen( 47 | navController = navController 48 | ) 49 | } 50 | 51 | composable( 52 | route = ModulesGraph.View.way, 53 | arguments = listOf(navArgument("id") { type = NavType.StringType }), 54 | enterTransition = { slideInLeftToRight() }, 55 | exitTransition = { slideOutRightToLeft() } 56 | ) { 57 | ViewModuleScreen( 58 | navController = navController 59 | ) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/navigation/graph/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.navigation.graph 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavGraphBuilder 5 | import com.google.accompanist.navigation.animation.composable 6 | import com.google.accompanist.navigation.animation.navigation 7 | import com.sanmer.mrepo.ui.animate.slideInLeftToRight 8 | import com.sanmer.mrepo.ui.animate.slideInRightToLeft 9 | import com.sanmer.mrepo.ui.animate.slideOutLeftToRight 10 | import com.sanmer.mrepo.ui.animate.slideOutRightToLeft 11 | import com.sanmer.mrepo.ui.navigation.MainGraph 12 | import com.sanmer.mrepo.ui.screens.apptheme.AppThemeScreen 13 | import com.sanmer.mrepo.ui.screens.repository.RepositoryScreen 14 | import com.sanmer.mrepo.ui.screens.settings.SettingsScreen 15 | 16 | sealed class SettingsGraph(val route: String) { 17 | object Settings : SettingsGraph("settings") 18 | object AppTheme : SettingsGraph("appTheme") 19 | object Repo : SettingsGraph("repo") 20 | } 21 | 22 | fun NavGraphBuilder.settingsGraph( 23 | navController: NavController 24 | ) = navigation( 25 | startDestination = SettingsGraph.Settings.route, 26 | route = MainGraph.Settings.route 27 | ) { 28 | composable( 29 | route = SettingsGraph.Settings.route, 30 | enterTransition = { 31 | when (initialState.destination.route) { 32 | SettingsGraph.AppTheme.route, 33 | SettingsGraph.Repo.route -> slideInLeftToRight() 34 | else -> null 35 | } 36 | }, 37 | exitTransition = { 38 | when (initialState.destination.route) { 39 | SettingsGraph.AppTheme.route, 40 | SettingsGraph.Repo.route -> slideOutRightToLeft() 41 | else -> null 42 | } 43 | } 44 | ) { 45 | SettingsScreen( 46 | navController = navController 47 | ) 48 | } 49 | 50 | composable( 51 | route = SettingsGraph.AppTheme.route, 52 | enterTransition = { slideInRightToLeft() }, 53 | exitTransition = { slideOutLeftToRight() } 54 | ) { 55 | AppThemeScreen( 56 | navController = navController 57 | ) 58 | } 59 | 60 | composable( 61 | route = SettingsGraph.Repo.route, 62 | enterTransition = { slideInRightToLeft() }, 63 | exitTransition = { slideOutLeftToRight() } 64 | ) { 65 | RepositoryScreen( 66 | navController = navController 67 | ) 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/screens/apptheme/AppThemeScreen.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.screens.apptheme 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.WindowInsets 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.material3.TopAppBarDefaults 11 | import androidx.compose.material3.TopAppBarScrollBehavior 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.input.nestedscroll.nestedScroll 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.navigation.NavController 17 | import com.sanmer.mrepo.R 18 | import com.sanmer.mrepo.ui.component.SettingTitleItem 19 | import com.sanmer.mrepo.ui.utils.NavigateUpTopBar 20 | import com.sanmer.mrepo.ui.utils.navigateBack 21 | import com.sanmer.mrepo.ui.utils.none 22 | 23 | @Composable 24 | fun AppThemeScreen( 25 | navController: NavController 26 | ) { 27 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() 28 | 29 | BackHandler { navController.navigateBack() } 30 | 31 | Scaffold( 32 | modifier = Modifier 33 | .nestedScroll(scrollBehavior.nestedScrollConnection), 34 | topBar = { 35 | AppThemeTopBar( 36 | scrollBehavior = scrollBehavior, 37 | navController = navController 38 | ) 39 | }, 40 | contentWindowInsets = WindowInsets.none 41 | ) { innerPadding -> 42 | Column( 43 | modifier = Modifier 44 | .verticalScroll(rememberScrollState()) 45 | .padding(innerPadding), 46 | ) { 47 | SettingTitleItem(text = stringResource(id = R.string.app_theme_example)) 48 | ExampleItem() 49 | 50 | SettingTitleItem(text = stringResource(id = R.string.app_theme_palette)) 51 | ThemePaletteItem() 52 | 53 | SettingTitleItem(text = stringResource(id = R.string.app_theme_dark_theme)) 54 | DarkModeItem() 55 | } 56 | } 57 | } 58 | 59 | @Composable 60 | private fun AppThemeTopBar( 61 | scrollBehavior: TopAppBarScrollBehavior, 62 | navController: NavController 63 | ) = NavigateUpTopBar( 64 | title = R.string.page_app_theme, 65 | scrollBehavior = scrollBehavior, 66 | navController = navController 67 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/screens/home/MenuItem.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.screens.home 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.os.PowerManager 6 | import androidx.annotation.StringRes 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.DropdownMenuItem 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.DpOffset 14 | import androidx.compose.ui.unit.dp 15 | import com.sanmer.mrepo.R 16 | import com.sanmer.mrepo.ui.component.DropdownMenu 17 | import com.sanmer.mrepo.utils.SvcPower 18 | 19 | private sealed class Menu( 20 | @StringRes val label: Int, 21 | val reason: String, 22 | ) { 23 | object Reboot : Menu( 24 | label = R.string.menu_reboot, 25 | reason = "" 26 | ) 27 | object Recovery : Menu( 28 | label = R.string.menu_reboot_recovery, 29 | reason = "recovery" 30 | ) 31 | object Userspace : Menu( 32 | label = R.string.menu_reboot_userspace, 33 | reason = "userspace" 34 | ) 35 | object Bootloader : Menu( 36 | label = R.string.menu_reboot_bootloader, 37 | reason = "bootloader" 38 | ) 39 | object Download : Menu( 40 | label = R.string.menu_reboot_download, 41 | reason = "download" 42 | ) 43 | object EDL : Menu( 44 | label = R.string.menu_reboot_edl, 45 | reason = "edl" 46 | ) 47 | } 48 | 49 | private val options = mutableListOf( 50 | Menu.Reboot, 51 | Menu.Recovery, 52 | Menu.Bootloader, 53 | Menu.Download, 54 | Menu.EDL 55 | ) 56 | 57 | @Composable 58 | fun MenuItem( 59 | expanded: Boolean, 60 | onClose: () -> Unit 61 | ) = DropdownMenu( 62 | expanded = expanded, 63 | onDismissRequest = onClose, 64 | offset = DpOffset(0.dp, 5.dp), 65 | shape = RoundedCornerShape(15.dp) 66 | ) { 67 | val powerManager = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R 69 | && powerManager?.isRebootingUserspaceSupported == true 70 | ) { 71 | options.add(1, Menu.Userspace) 72 | } 73 | 74 | options.forEach { 75 | MenuItem( 76 | value = it, 77 | onClose = onClose 78 | ) 79 | } 80 | } 81 | 82 | @Composable 83 | private fun MenuItem( 84 | value: Menu, 85 | onClose: () -> Unit 86 | ) = DropdownMenuItem( 87 | text = { Text(text = stringResource(id = value.label)) }, 88 | onClick = { 89 | SvcPower.reboot(value.reason) 90 | onClose() 91 | } 92 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/screens/home/NonRootItem.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.screens.home 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.painterResource 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import com.sanmer.mrepo.R 16 | 17 | @Composable 18 | fun NonRootItem() = Surface( 19 | shape = RoundedCornerShape(20.dp), 20 | color = MaterialTheme.colorScheme.surface, 21 | tonalElevation = 2.dp 22 | ) { 23 | Row( 24 | modifier = Modifier 25 | .padding(all = 20.dp) 26 | .fillMaxWidth(), 27 | verticalAlignment = Alignment.CenterVertically, 28 | horizontalArrangement = Arrangement.Start 29 | ) { 30 | Icon( 31 | modifier = Modifier 32 | .size(28.dp), 33 | painter = painterResource(id = R.drawable.slash_outline), 34 | contentDescription = null 35 | ) 36 | 37 | Spacer(modifier = Modifier.width(16.dp)) 38 | 39 | Column( 40 | modifier = Modifier.fillMaxWidth(), 41 | verticalArrangement = Arrangement.spacedBy(4.dp) 42 | ) { 43 | Text( 44 | text = stringResource(id = R.string.non_root_title), 45 | style = MaterialTheme.typography.titleMedium, 46 | color = MaterialTheme.colorScheme.primary 47 | ) 48 | Text( 49 | text = stringResource(id = R.string.non_root_desc), 50 | style = MaterialTheme.typography.bodyMedium, 51 | ) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/screens/modules/MenuItem.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.screens.modules 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import androidx.compose.foundation.pager.PagerState 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.DropdownMenuItem 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.unit.DpOffset 12 | import androidx.compose.ui.unit.dp 13 | import androidx.hilt.navigation.compose.hiltViewModel 14 | import com.sanmer.mrepo.R 15 | import com.sanmer.mrepo.ui.component.DropdownMenu 16 | import com.sanmer.mrepo.viewmodel.ModulesViewModel 17 | 18 | private sealed class Menu( 19 | @StringRes val label: Int, 20 | @DrawableRes val icon: Int 21 | ) { 22 | object Cloud : Menu( 23 | label = R.string.modules_menu_cloud, 24 | icon = R.drawable.cloud_change_outline 25 | ) 26 | object Local : Menu( 27 | label = R.string.modules_menu_local, 28 | icon = R.drawable.rotate_outline 29 | ) 30 | } 31 | 32 | private val options = listOf( 33 | Menu.Cloud, 34 | Menu.Local 35 | ) 36 | 37 | @Composable 38 | fun MenuItem( 39 | expanded: Boolean, 40 | pagerState: PagerState, 41 | viewModel: ModulesViewModel = hiltViewModel(), 42 | onClose: () -> Unit 43 | ) = DropdownMenu( 44 | expanded = expanded, 45 | onDismissRequest = onClose, 46 | offset = DpOffset(0.dp, 5.dp), 47 | shape = RoundedCornerShape(15.dp) 48 | ) { 49 | DropdownMenuItem( 50 | text = { Text(text = stringResource(id = R.string.menu_scroll_top)) }, 51 | onClick = { 52 | viewModel.scrollToTop(pagerState.currentPage) 53 | onClose() 54 | } 55 | ) 56 | 57 | DropdownMenuItem( 58 | text = { Text(text = stringResource(id = R.string.menu_scroll_bottom)) }, 59 | onClick = { 60 | viewModel.scrollToBottom(pagerState.currentPage) 61 | onClose() 62 | } 63 | ) 64 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/screens/modules/SegmentedButtonsItem.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.screens.modules 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.pager.PagerState 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TopAppBarScrollBehavior 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.rememberCoroutineScope 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.text.style.TextOverflow 15 | import androidx.compose.ui.unit.dp 16 | import com.sanmer.mrepo.datastore.UserData 17 | import com.sanmer.mrepo.ui.component.AppBarContainerColor 18 | import com.sanmer.mrepo.ui.component.Segment 19 | import com.sanmer.mrepo.ui.component.SegmentedButtons 20 | import kotlinx.coroutines.launch 21 | 22 | @Composable 23 | fun SegmentedButtonsItem( 24 | state: PagerState, 25 | userData: UserData, 26 | scrollBehavior: TopAppBarScrollBehavior, 27 | modifier: Modifier = Modifier 28 | ) = AppBarContainerColor(scrollBehavior) { containerColor -> 29 | Surface( 30 | modifier = modifier.fillMaxWidth(), 31 | color = containerColor 32 | ) { 33 | SegmentedButtonsItem( 34 | modifier = Modifier 35 | .padding(bottom = 10.dp, start = 20.dp, end = 20.dp), 36 | state = state, 37 | userData = userData 38 | ) 39 | } 40 | } 41 | 42 | @Composable 43 | private fun SegmentedButtonsItem( 44 | state: PagerState, 45 | userData: UserData, 46 | modifier: Modifier = Modifier 47 | ) { 48 | val scope = rememberCoroutineScope() 49 | 50 | SegmentedButtons( 51 | modifier = modifier 52 | ) { 53 | pages.forEachIndexed { id, page -> 54 | Segment( 55 | selected = state.currentPage == id, 56 | onClick = { 57 | scope.launch { 58 | state.animateScrollToPage(id) 59 | } 60 | }, 61 | enabled = if (page !is Pages.Cloud) userData.isRoot else true 62 | ) { 63 | Text( 64 | text = stringResource(id = page.label), 65 | style = MaterialTheme.typography.titleSmall, 66 | maxLines = 1, 67 | overflow = TextOverflow.Ellipsis 68 | ) 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.theme 2 | 3 | import androidx.compose.material3.Shapes 4 | 5 | val Shapes = Shapes() -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.SideEffect 6 | import androidx.compose.ui.graphics.Color 7 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 8 | 9 | @Composable 10 | fun AppTheme( 11 | darkMode: Boolean, 12 | themeColor: Int, 13 | content: @Composable () -> Unit 14 | ) { 15 | val systemUiController = rememberSystemUiController() 16 | SideEffect { 17 | systemUiController.setSystemBarsColor( 18 | color = Color.Transparent, 19 | darkIcons = !darkMode, 20 | isNavigationBarContrastEnforced = false 21 | ) 22 | } 23 | 24 | val color = getColor(id = themeColor) 25 | val colorScheme = when { 26 | darkMode -> color.darkColorScheme 27 | else -> color.lightColorScheme 28 | } 29 | 30 | MaterialTheme( 31 | colorScheme = colorScheme, 32 | shapes = Shapes, 33 | typography = Typography, 34 | content = content 35 | ) 36 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val Typography = Typography() -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/utils/LazyListState.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.utils 2 | 3 | import androidx.compose.foundation.lazy.LazyListState 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.State 6 | import androidx.compose.runtime.derivedStateOf 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.setValue 11 | 12 | @Composable 13 | fun LazyListState.isScrollingUp(): State { 14 | var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) } 15 | var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) } 16 | return remember(this) { 17 | derivedStateOf { 18 | if (previousIndex != firstVisibleItemIndex) { 19 | previousIndex > firstVisibleItemIndex 20 | } else { 21 | previousScrollOffset >= firstVisibleItemScrollOffset 22 | }.also { 23 | previousIndex = firstVisibleItemIndex 24 | previousScrollOffset = firstVisibleItemScrollOffset 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/utils/Logo.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.utils 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.MaterialTheme 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.draw.shadow 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.unit.dp 18 | 19 | @Composable 20 | fun Logo( 21 | @DrawableRes iconRes: Int, 22 | modifier: Modifier = Modifier, 23 | contentColor: Color = MaterialTheme.colorScheme.onPrimary, 24 | backgroundColor: Color = MaterialTheme.colorScheme.primary 25 | ) { 26 | Box( 27 | modifier = modifier 28 | .clip(CircleShape) 29 | .shadow(elevation = 10.dp) 30 | .background(color = backgroundColor), 31 | contentAlignment = Alignment.Center 32 | ) { 33 | Icon( 34 | modifier = Modifier 35 | .fillMaxSize(0.6f), 36 | painter = painterResource(id = iconRes), 37 | contentDescription = null, 38 | tint = contentColor 39 | ) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/utils/NavController.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.utils 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavGraph.Companion.findStartDestination 5 | import androidx.navigation.NavOptionsBuilder 6 | 7 | fun NavController.navigateSingleTopTo( 8 | route: String, 9 | builder: NavOptionsBuilder.() -> Unit = {} 10 | ) { 11 | this.navigate( 12 | route = route, 13 | ) { 14 | launchSingleTop = true 15 | restoreState = true 16 | builder(this) 17 | } 18 | } 19 | 20 | fun NavController.navigatePopUpTo( 21 | route: String 22 | ) { 23 | navigateSingleTopTo( 24 | route = route, 25 | ) { 26 | popUpTo(graph.findStartDestination().id) { 27 | saveState = true 28 | } 29 | } 30 | } 31 | 32 | fun NavController.navigateBack() { 33 | val route = currentBackStackEntry?.destination?.parent?.route 34 | if (route.isNullOrEmpty()) { 35 | navigateUp() 36 | } else { 37 | navigatePopUpTo(route) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/utils/PaddingValues.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.utils 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.ui.unit.Dp 5 | import androidx.compose.ui.unit.dp 6 | 7 | fun fabPadding(all: Dp = 0.dp) = PaddingValues( 8 | top = all, 9 | bottom = all + 80.dp, 10 | start = all, 11 | end = all 12 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/utils/Text.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.utils 2 | 3 | import android.text.method.LinkMovementMethod 4 | import android.widget.TextView 5 | import androidx.compose.material3.LocalContentColor 6 | import androidx.compose.material3.LocalTextStyle 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.graphics.toArgb 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.text.TextStyle 14 | import androidx.compose.ui.viewinterop.AndroidView 15 | import androidx.core.text.HtmlCompat 16 | import io.noties.markwon.Markwon 17 | 18 | @Composable 19 | fun HtmlText( 20 | text: String, 21 | modifier: Modifier = Modifier, 22 | style: TextStyle = LocalTextStyle.current, 23 | color: Color = LocalContentColor.current, 24 | ) { 25 | val linkTextColor = MaterialTheme.colorScheme.primary.toArgb() 26 | AndroidView( 27 | modifier = modifier, 28 | factory = { TextView(it) }, 29 | update = { 30 | it.movementMethod = LinkMovementMethod.getInstance() 31 | it.setLinkTextColor(linkTextColor) 32 | it.highlightColor = style.background.toArgb() 33 | 34 | it.textSize = style.fontSize.value 35 | it.setTextColor(color.toArgb()) 36 | it.setBackgroundColor(style.background.toArgb()) 37 | it.text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_COMPACT) 38 | } 39 | ) 40 | } 41 | 42 | @Composable 43 | fun MarkdownText( 44 | text: String, 45 | modifier: Modifier = Modifier, 46 | style: TextStyle = LocalTextStyle.current, 47 | color: Color = LocalContentColor.current, 48 | ) { 49 | val context = LocalContext.current 50 | val markdown = Markwon.create(context) 51 | val linkTextColor = MaterialTheme.colorScheme.primary.toArgb() 52 | 53 | AndroidView( 54 | modifier = modifier, 55 | factory = { TextView(it) }, 56 | update = { 57 | it.setLinkTextColor(linkTextColor) 58 | it.highlightColor = style.background.toArgb() 59 | 60 | it.textSize = style.fontSize.value 61 | it.setTextColor(color.toArgb()) 62 | it.setBackgroundColor(style.background.toArgb()) 63 | markdown.setMarkdown(it, text) 64 | } 65 | ) 66 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/ui/utils/WindowInsets.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.ui.utils 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | 5 | val WindowInsets.Companion.none get() = WindowInsets(0, 0, 0, 0) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/HttpUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils 2 | 3 | import com.sanmer.mrepo.utils.expansion.runRequest 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.adapter 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import okhttp3.OkHttpClient 9 | import okhttp3.Request 10 | import okhttp3.ResponseBody 11 | import java.io.OutputStream 12 | 13 | object HttpUtils { 14 | suspend inline fun request( 15 | url: String, 16 | crossinline get: (ResponseBody) -> T 17 | ) = withContext(Dispatchers.IO) { 18 | runRequest(get = get) { 19 | val client = OkHttpClient() 20 | val request = Request.Builder() 21 | .url(url) 22 | .build() 23 | client.newCall(request).execute() 24 | } 25 | } 26 | 27 | suspend fun requestString( 28 | url: String 29 | ): Result = request( 30 | url = url, 31 | get = { it.string() } 32 | ) 33 | 34 | suspend inline fun requestJson( 35 | url: String 36 | ): Result = request(url) { 37 | val adapter = Moshi.Builder() 38 | .build() 39 | .adapter() 40 | 41 | return@request adapter.fromJson(it.string())!! 42 | } 43 | 44 | suspend fun downloader( 45 | url: String, 46 | output: OutputStream, 47 | onProgress: (Float) -> Unit 48 | ): Result = request(url) { body -> 49 | val buffer = ByteArray(2048) 50 | val input = body.byteStream() 51 | 52 | val all = body.contentLength() 53 | var finished: Long = 0 54 | var readying: Int 55 | 56 | while (input.read(buffer).also { readying = it } != -1) { 57 | output.write(buffer, 0, readying) 58 | finished += readying.toLong() 59 | 60 | val progress = (finished * 1.0 / all).toFloat() 61 | onProgress(progress) 62 | } 63 | 64 | output.flush() 65 | output.close() 66 | input.close() 67 | 68 | return@request true 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/SvcPower.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils 2 | 3 | import com.topjohnwu.superuser.Shell 4 | 5 | object SvcPower { 6 | fun reboot(reason: String = "") { 7 | if (reason == "recovery") { 8 | // KEYCODE_POWER = 26, hide incorrect "Factory data reset" message 9 | Shell.cmd("/system/bin/input keyevent 26").submit() 10 | } 11 | Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit() 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/Context.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.app.ShareCompat 6 | import androidx.core.content.FileProvider 7 | import com.sanmer.mrepo.BuildConfig 8 | import java.io.File 9 | 10 | fun Context.openUrl(url: String) { 11 | Intent.parseUri(url, Intent.URI_INTENT_SCHEME).apply { 12 | startActivity(this) 13 | } 14 | } 15 | 16 | fun Context.shareText(text: String) { 17 | ShareCompat.IntentBuilder(this) 18 | .setType("text/plain") 19 | .setText(text) 20 | .startChooser() 21 | } 22 | 23 | fun Context.shareFile(file: File, mimeType: String) { 24 | val uri = FileProvider.getUriForFile(this, 25 | "${BuildConfig.APPLICATION_ID}.provider", file) 26 | 27 | ShareCompat.IntentBuilder(this) 28 | .setType(mimeType) 29 | .addStream(uri) 30 | .startChooser() 31 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/List.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | import androidx.compose.runtime.snapshots.SnapshotStateList 4 | 5 | inline fun MutableList.update(value: T) { 6 | val index = indexOfFirst { it == value } 7 | if (index == -1) { 8 | add(value) 9 | } else { 10 | set(index, value) 11 | } 12 | } 13 | 14 | inline fun SnapshotStateList.update(value: T) { 15 | val index = indexOfFirst { it == value } 16 | if (index == -1) { 17 | add(value) 18 | } else { 19 | removeAt(index) 20 | add(index, value) 21 | } 22 | } 23 | 24 | inline fun List>.merge(): List { 25 | val values = mutableListOf() 26 | forEach { values.addAll(it) } 27 | return values 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/LocalDateTime.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | import kotlinx.datetime.* 4 | 5 | fun Float.toDateTime(): String { 6 | val instant = Instant.fromEpochMilliseconds(times(1000).toLong()) 7 | return instant.toLocalDateTime(TimeZone.currentSystemDefault()).toString() 8 | } 9 | 10 | fun Float.toDate(): String { 11 | val instant = Instant.fromEpochMilliseconds(times(1000).toLong()) 12 | return instant.toLocalDateTime(TimeZone.currentSystemDefault()).date.toString() 13 | } 14 | 15 | fun LocalDateTime.Companion.now() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/Parcelable.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | import android.content.Intent 4 | import android.os.Build.VERSION.SDK_INT 5 | import android.os.Build.VERSION_CODES.TIRAMISU 6 | import android.os.Bundle 7 | import android.os.Parcelable 8 | 9 | inline fun Intent.parcelable(key: String): T? = when { 10 | SDK_INT >= TIRAMISU -> getParcelableExtra(key, T::class.java) 11 | else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T 12 | } 13 | 14 | inline fun Bundle.parcelable(key: String): T? = when { 15 | SDK_INT >= TIRAMISU -> getParcelable(key, T::class.java) 16 | else -> @Suppress("DEPRECATION") getParcelable(key) as? T 17 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/Response.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | inline fun runRequest( 4 | run: () -> retrofit2.Response 5 | ): Result = try { 6 | val response = run() 7 | if (response.isSuccessful) { 8 | val data = response.body() 9 | if (data != null) { 10 | Result.success(data) 11 | }else { 12 | Result.failure(NullPointerException("The data is null!")) 13 | } 14 | } else { 15 | val errorBody = response.errorBody() 16 | val error = errorBody?.string() ?: "404 Not Found" 17 | 18 | if ("" in error) { 19 | Result.failure(RuntimeException("404 Not Found")) 20 | } else { 21 | Result.failure(RuntimeException(error)) 22 | } 23 | } 24 | } catch (e: Exception) { 25 | Result.failure(e) 26 | } 27 | 28 | inline fun runRequest( 29 | get: (okhttp3.ResponseBody) -> T, 30 | run: () -> okhttp3.Response 31 | ): Result = try { 32 | val response = run() 33 | if (response.isSuccessful) { 34 | val data = response.body() 35 | if (data != null) { 36 | Result.success(get(data)) 37 | } else { 38 | Result.failure(NullPointerException("The data is null!")) 39 | } 40 | } else { 41 | val errorBody = response.body() 42 | val error = errorBody?.string() ?: "404 Not Found" 43 | 44 | if ("" in error) { 45 | Result.failure(RuntimeException("404 Not Found")) 46 | } else { 47 | Result.failure(RuntimeException(error)) 48 | } 49 | } 50 | } catch (e: Exception) { 51 | Result.failure(e) 52 | } 53 | 54 | inline fun runRequest( 55 | run: () -> retrofit2.Response, 56 | convert: (T) -> R 57 | ): Result = try { 58 | val response = run() 59 | if (response.isSuccessful) { 60 | val data = response.body() 61 | if (data != null) { 62 | Result.success(convert(data)) 63 | }else { 64 | Result.failure(NullPointerException("The data is null!")) 65 | } 66 | } else { 67 | val errorBody = response.errorBody() 68 | val error = errorBody?.string() ?: "404 Not Found" 69 | 70 | if ("" in error) { 71 | Result.failure(RuntimeException("404 Not Found")) 72 | } else { 73 | Result.failure(RuntimeException(error)) 74 | } 75 | } 76 | } catch (e: Exception) { 77 | Result.failure(e) 78 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/Result.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | import com.topjohnwu.superuser.Shell.Result 4 | 5 | val Result.output get() = out.joinToString().trim() -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/String.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | import java.io.File 4 | 5 | fun String.toLongOr(v: Long): Long { 6 | if (isEmpty() || isBlank()) return v 7 | 8 | return try { 9 | toLong() 10 | } catch (e: NumberFormatException) { 11 | v 12 | } 13 | } 14 | 15 | fun String.toLongOrZero(): Long = toLongOr(0) 16 | 17 | fun String.toFile() = File(this) 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/expansion/ZipUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.expansion 2 | 3 | import java.io.File 4 | import java.io.IOException 5 | import java.io.InputStream 6 | import java.util.zip.ZipEntry 7 | import java.util.zip.ZipInputStream 8 | 9 | @Throws(IOException::class) 10 | fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) { 11 | inputStream().buffered().use { 12 | it.unzip(folder, path, junkPath) 13 | } 14 | } 15 | 16 | @Throws(IOException::class) 17 | fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) { 18 | try { 19 | val zin = ZipInputStream(this) 20 | var entry: ZipEntry 21 | while (true) { 22 | entry = zin.nextEntry ?: break 23 | if (!entry.name.startsWith(path) || entry.isDirectory) { 24 | // Ignore directories, only create files 25 | continue 26 | } 27 | val name = if (junkPath) 28 | entry.name.substring(entry.name.lastIndexOf('/') + 1) 29 | else 30 | entry.name 31 | 32 | val dest = File(folder, name) 33 | dest.parentFile!!.let { 34 | if (!it.exists()) 35 | it.mkdirs() 36 | } 37 | dest.outputStream().use { out -> zin.copyTo(out) } 38 | } 39 | } catch (e: IllegalArgumentException) { 40 | throw IOException(e) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/log/LogText.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.log 2 | 3 | import com.sanmer.mrepo.utils.log.Logcat.toLogPriority 4 | import com.sanmer.mrepo.utils.log.Logcat.toTextPriority 5 | 6 | data class LogText( 7 | val priority: Int, 8 | val time: String, 9 | val process: String, 10 | val tag: String, 11 | val message: String 12 | ) { 13 | override fun toString(): String { 14 | return "[ $time $process ${priority.toTextPriority()}/$tag ] $message" 15 | } 16 | 17 | companion object { 18 | fun parse(text: String): LogText { 19 | val texts = text.split("/", limit = 2) 20 | val tmp1 = texts.first() 21 | .replace("[ ", "") 22 | .split(" ", limit = 4) 23 | val tmp2 = texts.last().split(" ] ", limit = 2) 24 | 25 | return LogText( 26 | priority = tmp1[3].toLogPriority(), 27 | time = "${tmp1[0]} ${tmp1[1]}", 28 | process = tmp1[2], 29 | tag = tmp2[0], 30 | message = tmp2[1] 31 | ) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/timber/DebugTree.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.timber 2 | 3 | import android.util.Log 4 | import timber.log.Timber 5 | 6 | class DebugTree : Timber.DebugTree() { 7 | override fun isLoggable(tag: String?, priority: Int): Boolean { 8 | return when (priority) { 9 | Log.VERBOSE -> true 10 | Log.DEBUG -> true 11 | Log.INFO -> true 12 | Log.WARN -> true 13 | Log.ERROR -> true 14 | else -> false 15 | } 16 | } 17 | 18 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 19 | if (!isLoggable(tag, priority)) { 20 | return 21 | } 22 | 23 | super.log(priority, "MRepo", "$tag --> $message", t) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/utils/timber/ReleaseTree.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.utils.timber 2 | 3 | import android.util.Log 4 | import timber.log.Timber 5 | 6 | class ReleaseTree : Timber.DebugTree() { 7 | override fun isLoggable(tag: String?, priority: Int): Boolean { 8 | return when (priority) { 9 | Log.VERBOSE -> true 10 | Log.DEBUG -> true 11 | Log.INFO -> true 12 | Log.WARN -> true 13 | Log.ERROR -> true 14 | else -> false 15 | } 16 | } 17 | 18 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 19 | if (!isLoggable(tag, priority)) { 20 | return 21 | } 22 | 23 | super.log(priority, "MRepo", message, t) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/viewmodel/RepositoryViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.viewmodel 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.compose.runtime.toMutableStateList 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import com.sanmer.mrepo.database.entity.Repo 10 | import com.sanmer.mrepo.database.entity.toRepo 11 | import com.sanmer.mrepo.repository.LocalRepository 12 | import com.sanmer.mrepo.repository.ModulesRepository 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.flow.map 15 | import kotlinx.coroutines.launch 16 | import timber.log.Timber 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class RepositoryViewModel @Inject constructor( 21 | private val localRepository: LocalRepository, 22 | private val modulesRepository: ModulesRepository 23 | ) : ViewModel() { 24 | 25 | val list = localRepository.getRepoAllAsFlow().map { list -> 26 | list.toMutableStateList().sortedBy { it.name } 27 | } 28 | 29 | var progress by mutableStateOf(false) 30 | private set 31 | 32 | private inline fun T.updateProgress(callback: T.() -> Unit) { 33 | progress = true 34 | callback() 35 | progress = false 36 | } 37 | 38 | init { 39 | Timber.d("RepositoryViewModel init") 40 | } 41 | 42 | fun insert( 43 | repoUrl: String, 44 | onFailure: (Repo, Throwable) -> Unit 45 | ) = viewModelScope.launch { 46 | updateProgress { 47 | val repo = repoUrl.toRepo() 48 | 49 | modulesRepository.getRepo(repo) 50 | .onSuccess { 51 | localRepository.insertRepo(it) 52 | }.onFailure { 53 | onFailure(repo, it) 54 | } 55 | } 56 | } 57 | 58 | fun update(repo: Repo) = viewModelScope.launch { 59 | localRepository.updateRepo(repo) 60 | } 61 | 62 | fun delete(repo: Repo) = viewModelScope.launch { 63 | localRepository.deleteRepo(repo) 64 | localRepository.deleteOnlineByUrl(repo.url) 65 | } 66 | 67 | fun getUpdate( 68 | repo: Repo, 69 | onFailure: (Throwable) -> Unit 70 | ) = viewModelScope.launch { 71 | updateProgress { 72 | modulesRepository.getRepo(repo) 73 | .onSuccess { 74 | localRepository.updateRepo(it) 75 | }.onFailure(onFailure) 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/works/LocalWork.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.works 2 | 3 | import android.content.Context 4 | import androidx.hilt.work.HiltWorker 5 | import androidx.work.BackoffPolicy 6 | import androidx.work.CoroutineWorker 7 | import androidx.work.OneTimeWorkRequestBuilder 8 | import androidx.work.WorkerParameters 9 | import com.sanmer.mrepo.repository.ModulesRepository 10 | import dagger.assisted.Assisted 11 | import dagger.assisted.AssistedInject 12 | import timber.log.Timber 13 | import java.util.concurrent.TimeUnit 14 | 15 | @HiltWorker 16 | class LocalWork @AssistedInject constructor( 17 | @Assisted context: Context, 18 | @Assisted workerParams: WorkerParameters, 19 | private val modulesRepository: ModulesRepository 20 | ) : CoroutineWorker( 21 | context, 22 | workerParams 23 | ) { 24 | override suspend fun doWork(): Result { 25 | Timber.d("LocalWork: doWork") 26 | val result = modulesRepository.getLocalAll() 27 | 28 | return if (result.isSuccess) { 29 | Result.success() 30 | } else { 31 | Result.retry() 32 | } 33 | } 34 | 35 | companion object { 36 | val OneTimeWork = OneTimeWorkRequestBuilder() 37 | .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) 38 | .build() 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/sanmer/mrepo/works/RepoWork.kt: -------------------------------------------------------------------------------- 1 | package com.sanmer.mrepo.works 2 | 3 | import android.content.Context 4 | import androidx.hilt.work.HiltWorker 5 | import androidx.work.BackoffPolicy 6 | import androidx.work.Constraints 7 | import androidx.work.CoroutineWorker 8 | import androidx.work.NetworkType 9 | import androidx.work.OneTimeWorkRequestBuilder 10 | import androidx.work.WorkerParameters 11 | import com.sanmer.mrepo.repository.ModulesRepository 12 | import dagger.assisted.Assisted 13 | import dagger.assisted.AssistedInject 14 | import timber.log.Timber 15 | import java.util.concurrent.TimeUnit 16 | 17 | @HiltWorker 18 | class RepoWork @AssistedInject constructor( 19 | @Assisted context: Context, 20 | @Assisted workerParams: WorkerParameters, 21 | private val modulesRepository: ModulesRepository 22 | ) : CoroutineWorker( 23 | context, 24 | workerParams 25 | ) { 26 | override suspend fun doWork(): Result { 27 | Timber.d("RepoWork: doWork") 28 | val result = modulesRepository.getRepoAll() 29 | 30 | return if (result.all { it.isFailure }) { 31 | Result.retry() 32 | } else { 33 | Result.success() 34 | } 35 | } 36 | 37 | companion object { 38 | val OneTimeWork = OneTimeWorkRequestBuilder() 39 | .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) 40 | .setConstraints( 41 | Constraints.Builder() 42 | .setRequiredNetworkType(NetworkType.CONNECTED) 43 | .build() 44 | ) 45 | .build() 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fox2Code/MRepo/02f9eddb5dffeb38f99bc48a100b7746cd3337bd/app/src/main/playstore.png -------------------------------------------------------------------------------- /app/src/main/proto/com/sanmer/mrepo/datastore/UserPreferences.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_package = "com.sanmer.mrepo.datastore"; 4 | option java_multiple_files = true; 5 | 6 | enum WorkingMode { 7 | FIRST_SETUP = 0; 8 | MODE_ROOT = 1; 9 | MODE_NON_ROOT = 2; 10 | } 11 | 12 | enum DarkMode { 13 | FOLLOW_SYSTEM = 0; 14 | ALWAYS_OFF = 1; 15 | ALWAYS_ON = 2; 16 | } 17 | 18 | message UserPreferences { 19 | WorkingMode workingMode = 1; 20 | DarkMode darkMode = 2; 21 | int32 themeColor = 3; 22 | string downloadPath = 4; 23 | bool deleteZipFile = 5; 24 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/add_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow_down_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow_right_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow_square_left_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/auto_brightness_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/box_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/box_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bucket_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/close_square_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cloud_change_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cloud_connection_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/cube_scan_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/danger_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/directbox_receive_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/document_code_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/document_text_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/flag_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/health_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/health_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/hierarchy_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/home_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/import_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/information_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/link_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/link_square_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/main_component_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/mobile_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/moon_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/osi.xml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/people_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/refresh_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rotate_left_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rotate_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/search_normal_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/send_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/setting_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/setting_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shortcut_log.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shortcut_modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shortcut_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/slash_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sort_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/square_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/star_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sun_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tick_circle_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tick_circle_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tick_square_bold.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/trash_outline.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-v31/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_accent1_600 4 | @android:color/system_accent3_100 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF9B404F 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings_untranslatable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MRepo 4 | 5 | 6 | @string/settings_app_theme 7 | @string/settings_log_viewer 8 | @string/settings_repo 9 | 10 | 11 | 12 | @string/setup_non_root_title 13 | 14 | @string/module_update 15 | 16 | 17 | 18 | @string/setup_mode 19 | @string/setup_root_title 20 | @string/setup_non_root_title 21 | 22 | @string/modules_page_cloud_empty 23 | 24 | 25 | @string/page_modules 26 | @string/page_settings 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |