├── .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 | [](https://github.com/ya0211/MRepo/releases) [](https://github.com/ya0211/MRepo/releases) [](LICENSE) [](https://t.me/mrepo_news) [](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 ("