├── fastlane
└── metadata
│ └── android
│ ├── ar
│ ├── title.txt
│ ├── short_description.txt
│ └── full_description.txt
│ ├── en-US
│ ├── title.txt
│ ├── short_description.txt
│ ├── images
│ │ ├── icon.png
│ │ └── phoneScreenshots
│ │ │ ├── 1.png
│ │ │ ├── 2.png
│ │ │ ├── 3.png
│ │ │ ├── 4.png
│ │ │ ├── 5.png
│ │ │ └── 6.png
│ └── full_description.txt
│ └── ru-RU
│ ├── title.txt
│ ├── short_description.txt
│ └── full_description.txt
├── app
├── proguard-rules.pro
└── src
│ └── main
│ ├── res
│ ├── values-fil
│ │ └── strings.xml
│ ├── values-night
│ │ ├── themes.xml
│ │ └── colors.xml
│ ├── drawable
│ │ ├── launcher_splash.xml
│ │ ├── check.xml
│ │ ├── arrow_badge_up.xml
│ │ ├── arrow_badge_down.xml
│ │ ├── rotate.xml
│ │ ├── brand_open_source.xml
│ │ ├── command.xml
│ │ ├── search.xml
│ │ ├── cloud.xml
│ │ ├── refresh.xml
│ │ ├── reload.xml
│ │ ├── currency_dollar.xml
│ │ ├── notification.xml
│ │ ├── circle_check_filled.xml
│ │ ├── sun.xml
│ │ ├── alert_circle_filled.xml
│ │ ├── circle_x_filled.xml
│ │ ├── brightness_2.xml
│ │ ├── menu_2.xml
│ │ ├── arrow_left.xml
│ │ ├── hexagons.xml
│ │ ├── info_circle_filled.xml
│ │ ├── cloud_download.xml
│ │ ├── home.xml
│ │ ├── device_floppy.xml
│ │ ├── notification_off.xml
│ │ ├── award.xml
│ │ ├── alert_triangle.xml
│ │ ├── moon_stars.xml
│ │ ├── files.xml
│ │ ├── github.xml
│ │ ├── box.xml
│ │ ├── pencil_plus.xml
│ │ ├── telegram.xml
│ │ ├── users.xml
│ │ ├── file_download.xml
│ │ ├── file_certificate.xml
│ │ ├── settings.xml
│ │ ├── color_swatch.xml
│ │ ├── heart_handshake.xml
│ │ ├── launcher_outline.xml
│ │ ├── trash.xml
│ │ ├── file_type_zip.xml
│ │ ├── device_mobile_down.xml
│ │ ├── share.xml
│ │ ├── world.xml
│ │ ├── package_import.xml
│ │ ├── git_pull_request.xml
│ │ ├── launcher_foreground.xml
│ │ ├── brand_git.xml
│ │ ├── ci_label.xml
│ │ └── weblate.xml
│ ├── values
│ │ ├── colors.xml
│ │ ├── strings_untranslatable.xml
│ │ └── themes.xml
│ ├── values-v31
│ │ └── colors.xml
│ ├── values-night-v31
│ │ └── colors.xml
│ ├── mipmap-anydpi-v26
│ │ └── launcher.xml
│ └── xml
│ │ └── locales_config.xml
│ ├── kotlin
│ └── dev
│ │ └── sanmer
│ │ └── mrepo
│ │ ├── ui
│ │ ├── theme
│ │ │ ├── Shape.kt
│ │ │ ├── Type.kt
│ │ │ └── Theme.kt
│ │ ├── providable
│ │ │ └── LocalUserPreferences.kt
│ │ ├── utils
│ │ │ ├── BottomSheetExt.kt
│ │ │ ├── MenuExt.kt
│ │ │ ├── NavControllerExt.kt
│ │ │ └── LazyListStateExt.kt
│ │ ├── navigation
│ │ │ ├── Main.kt
│ │ │ └── graphs
│ │ │ │ ├── Modules.kt
│ │ │ │ ├── Repository.kt
│ │ │ │ └── Settings.kt
│ │ ├── screens
│ │ │ ├── repository
│ │ │ │ ├── view
│ │ │ │ │ ├── items
│ │ │ │ │ │ ├── TagItem.kt
│ │ │ │ │ │ └── LicenseItem.kt
│ │ │ │ │ ├── pages
│ │ │ │ │ │ └── AboutPage.kt
│ │ │ │ │ └── ViewScreen.kt
│ │ │ │ └── ModulesList.kt
│ │ │ └── settings
│ │ │ │ ├── items
│ │ │ │ ├── NonRootItem.kt
│ │ │ │ └── RootItem.kt
│ │ │ │ ├── workingmode
│ │ │ │ ├── WorkingModeItem.kt
│ │ │ │ └── WorkingModeScreen.kt
│ │ │ │ ├── repositories
│ │ │ │ └── RepositoriesList.kt
│ │ │ │ └── app
│ │ │ │ └── items
│ │ │ │ └── AppThemeItem.kt
│ │ ├── component
│ │ │ ├── Logo.kt
│ │ │ ├── AppBar.kt
│ │ │ ├── LabelItem.kt
│ │ │ ├── DropdownMenu.kt
│ │ │ ├── TextFieldDialog.kt
│ │ │ ├── MenuChip.kt
│ │ │ ├── Text.kt
│ │ │ └── scrollbar
│ │ │ │ └── ThumbExt.kt
│ │ └── activity
│ │ │ ├── SetupScreen.kt
│ │ │ └── InstallActivity.kt
│ │ ├── datastore
│ │ ├── model
│ │ │ ├── Option.kt
│ │ │ ├── Homepage.kt
│ │ │ ├── DarkMode.kt
│ │ │ ├── ModulesMenu.kt
│ │ │ ├── WorkingMode.kt
│ │ │ ├── RepositoryMenu.kt
│ │ │ └── UserPreferences.kt
│ │ ├── UserPreferencesSerializer.kt
│ │ ├── di
│ │ │ └── DataStoreModule.kt
│ │ └── UserPreferencesDataSource.kt
│ │ ├── model
│ │ ├── local
│ │ │ ├── State.kt
│ │ │ └── LocalModule.kt
│ │ ├── json
│ │ │ ├── License.kt
│ │ │ └── UpdateJson.kt
│ │ └── online
│ │ │ ├── ModulesJson.kt
│ │ │ ├── VersionItem.kt
│ │ │ └── OnlineModule.kt
│ │ ├── utils
│ │ ├── extensions
│ │ │ ├── LocaleListCompatExt.kt
│ │ │ ├── ParcelableExt.kt
│ │ │ ├── LocalDateTimeExt.kt
│ │ │ └── ContextExt.kt
│ │ ├── timber
│ │ │ ├── ReleaseTree.kt
│ │ │ └── DebugTree.kt
│ │ └── StrUtil.kt
│ │ ├── stub
│ │ └── IRepoManager.kt
│ │ ├── app
│ │ ├── Const.kt
│ │ └── utils
│ │ │ └── NotificationUtils.kt
│ │ ├── compat
│ │ ├── BuildCompat.kt
│ │ └── PermissionCompat.kt
│ │ ├── App.kt
│ │ ├── database
│ │ ├── di
│ │ │ └── DatabaseModule.kt
│ │ ├── AppDatabase.kt
│ │ ├── entity
│ │ │ ├── online
│ │ │ │ ├── RepoEntity.kt
│ │ │ │ ├── VersionItemEntity.kt
│ │ │ │ └── OnlineModuleEntity.kt
│ │ │ └── local
│ │ │ │ └── LocalModuleEntity.kt
│ │ └── dao
│ │ │ ├── LocalDao.kt
│ │ │ └── RepoDao.kt
│ │ ├── repository
│ │ ├── UserPreferencesRepository.kt
│ │ └── ModulesRepository.kt
│ │ ├── viewmodel
│ │ ├── SettingsViewModel.kt
│ │ └── RepositoriesViewModel.kt
│ │ └── Compat.kt
│ └── AndroidManifest.xml
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── fr.yml
│ └── bug.yml
├── dependabot.yml
└── workflows
│ └── android.yml
├── core
├── src
│ └── main
│ │ ├── aidl
│ │ └── dev
│ │ │ └── sanmer
│ │ │ └── mrepo
│ │ │ ├── content
│ │ │ └── Module.aidl
│ │ │ └── stub
│ │ │ ├── IModuleOpsCallback.aidl
│ │ │ ├── IInstallCallback.aidl
│ │ │ └── IModuleManager.aidl
│ │ └── kotlin
│ │ └── dev
│ │ └── sanmer
│ │ └── mrepo
│ │ ├── Platform.kt
│ │ ├── content
│ │ ├── State.kt
│ │ └── Module.kt
│ │ ├── ModuleManager.kt
│ │ └── impl
│ │ ├── Shell.kt
│ │ ├── APatchModuleManagerImpl.kt
│ │ ├── KernelSUModuleManagerImpl.kt
│ │ └── MagiskModuleManagerImpl.kt
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── .gitignore
├── settings.gradle.kts
├── README.md
└── gradlew.bat
/fastlane/metadata/android/ar/title.txt:
--------------------------------------------------------------------------------
1 | MRepo
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | MRepo
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru-RU/title.txt:
--------------------------------------------------------------------------------
1 | MRepo
2 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -repackageclasses dev.sanmer.mrepo
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar/short_description.txt:
--------------------------------------------------------------------------------
1 | مدير إضافات لـMagisk وKernelSU
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | A modules manager for Magisk & KernelSU
2 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru-RU/short_description.txt:
--------------------------------------------------------------------------------
1 | Менеджер модулей для Magisk и KernelSU
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values-fil/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/src/main/aidl/dev/sanmer/mrepo/content/Module.aidl:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.content;
2 |
3 | parcelable Module;
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/mrepo/Platform.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo
2 |
3 | enum class Platform {
4 | Magisk,
5 | KernelSU,
6 | APatch
7 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.theme
2 |
3 | import androidx.compose.material3.Shapes
4 |
5 | val Shapes = Shapes()
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/model/Option.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore.model
2 |
3 | enum class Option {
4 | Name,
5 | UpdatedTime
6 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/model/local/State.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.model.local
2 |
3 | import dev.sanmer.mrepo.content.State
4 |
5 | typealias State = State
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MRepoApp/MRepo/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/model/Homepage.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore.model
2 |
3 | enum class Homepage {
4 | Modules,
5 | Repository
6 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/mrepo/content/State.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.content
2 |
3 | enum class State {
4 | Enable,
5 | Remove,
6 | Disable,
7 | Update
8 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/model/DarkMode.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore.model
2 |
3 | enum class DarkMode {
4 | FollowSystem,
5 | AlwaysOff,
6 | AlwaysOn
7 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/launcher_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #0A0C10
4 | #FBFCFD
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FBFCFD
4 | #0A0C10
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/system_accent1_600
4 | @android:color/system_accent1_0
5 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/utils/extensions/LocaleListCompatExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.utils.extensions
2 |
3 | import androidx.core.os.LocaleListCompat
4 | import java.util.Locale
5 |
6 | fun LocaleListCompat.toList(): List = List(size()) { this[it]!! }
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v31/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @android:color/system_accent1_200
4 | @android:color/system_accent1_800
5 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | org.gradle.caching=true
3 | org.gradle.configuration-cache=true
4 |
5 | android.useAndroidX=true
6 | android.nonTransitiveRClass=true
7 |
8 | kapt.include.compile.classpath=false
9 | kotlin.code.style=official
--------------------------------------------------------------------------------
/core/src/main/aidl/dev/sanmer/mrepo/stub/IModuleOpsCallback.aidl:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.stub;
2 |
3 | import dev.sanmer.su.wrap.ThrowableWrapper;
4 |
5 | oneway interface IModuleOpsCallback {
6 | void onSuccess(String id);
7 | void onFailure(String id, in ThrowableWrapper error);
8 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/model/local/LocalModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.model.local
2 |
3 | import dev.sanmer.mrepo.content.Module
4 | import dev.sanmer.mrepo.utils.StrUtil
5 |
6 | typealias LocalModule = Module
7 |
8 | val Module.versionDisplay get() = StrUtil.getVersionDisplay(version, versionCode)
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/providable/LocalUserPreferences.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.providable
2 |
3 | import androidx.compose.runtime.staticCompositionLocalOf
4 | import dev.sanmer.mrepo.datastore.model.UserPreferences
5 |
6 | val LocalUserPreferences = staticCompositionLocalOf { UserPreferences() }
7 |
--------------------------------------------------------------------------------
/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.self.library)
3 | alias(libs.plugins.kotlin.parcelize)
4 | }
5 |
6 | android {
7 | namespace = "dev.sanmer.mrepo.core"
8 |
9 | buildFeatures {
10 | aidl = true
11 | }
12 | }
13 |
14 | dependencies {
15 | api(libs.sanmer.su)
16 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/ar/full_description.txt:
--------------------------------------------------------------------------------
1 | MRepo هو تطبيق Android يساعد في إدارة مستودع الإضافت النمطية الخاص بك.
2 |
3 | المميزات:
4 | - إدارة مستودع الإضافات الخاصة بك
5 | - دعم مستودعات متعددة
6 | - دعم Magisk وKernelSU
7 | - إنشاء Jetpack وتصميم Material 3
8 |
9 | https://github.com/MRepoApp/MRepo
10 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | MRepo is an Android app that helps manage your own modules repository.
2 |
3 | Features:
4 | - Manage your modules repository
5 | - Support multiple repositories
6 | - Support Magisk and KernelSU
7 | - Jetpack Compose & Material Design 3
8 |
9 | https://github.com/MRepoApp/MRepo
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/utils/timber/ReleaseTree.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.utils.timber
2 |
3 | import timber.log.Timber
4 |
5 | class ReleaseTree : Timber.DebugTree() {
6 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
7 | super.log(priority, "$tag", message, t)
8 | }
9 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/ru-RU/full_description.txt:
--------------------------------------------------------------------------------
1 | Это приложение помогает управлять вашим собственным репозиторием модулей.
2 |
3 | Возможности:
4 | - Управление репозиторием модулей
5 | - Поддержка нескольких репозиториев
6 | - Поддержка Magisk и KernelSU
7 | - JetPack Compose и Материальный Дизайн 3
8 |
9 | https://github.com/mrepoapp/mrepo
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle
2 | .gradle/
3 | build/
4 |
5 | # Kotlin
6 | .kotlin
7 |
8 | # Local configuration
9 | local.properties
10 | signing.properties
11 |
12 | # Android Studio
13 | captures/
14 | release/
15 | .externalNativeBuild/
16 | .cxx/
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 |
22 | # Keystore
23 | *.jks
24 | *.keystore
25 |
26 | # MacOS
27 | .DS_Store
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/model/ModulesMenu.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class ModulesMenu(
7 | val option: Option = Option.Name,
8 | val descending: Boolean = false,
9 | val pinEnabled: Boolean = false,
10 | val showUpdatedTime: Boolean = false
11 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/src/main/aidl/dev/sanmer/mrepo/stub/IInstallCallback.aidl:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.stub;
2 |
3 | import dev.sanmer.mrepo.content.Module;
4 | import dev.sanmer.su.wrap.ThrowableWrapper;
5 |
6 | oneway interface IInstallCallback {
7 | void onStdout(String msg);
8 | void onStderr(String msg);
9 | void onSuccess(in Module module);
10 | void onFailure(in ThrowableWrapper error);
11 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/utils/BottomSheetExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.utils
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.BottomSheetDefaults
5 | import androidx.compose.ui.unit.Dp
6 |
7 | @Suppress("UnusedReceiverParameter")
8 | fun BottomSheetDefaults.expandedShape(size: Dp) =
9 | RoundedCornerShape(topStart = size, topEnd = size)
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/model/WorkingMode.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore.model
2 |
3 | enum class WorkingMode {
4 | Setup,
5 | None,
6 | Superuser,
7 | Shizuku;
8 |
9 | companion object {
10 | val WorkingMode.isRoot get() = this == Superuser || this == Shizuku
11 | val WorkingMode.isNonRoot get() = this == None
12 | val WorkingMode.isSetup get() = this == Setup
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/check.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_badge_up.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_badge_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rotate.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/mrepo/content/Module.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.content
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class Module(
8 | val id: String,
9 | val name: String,
10 | val version: String,
11 | val versionCode: Int,
12 | val author: String,
13 | val description: String,
14 | val updateJson: String,
15 | val state: State,
16 | val lastUpdated: Long
17 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/model/json/License.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.model.json
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
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 | val hasLabel by lazy {
15 | isFsfLibre || isOsiApproved
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/stub/IRepoManager.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.stub
2 |
3 | import dev.sanmer.mrepo.compat.NetworkCompat
4 | import dev.sanmer.mrepo.model.online.ModulesJson
5 | import retrofit2.Call
6 | import retrofit2.http.GET
7 |
8 | interface IRepoManager {
9 | @get:GET("json/modules.json")
10 | val modules: Call
11 |
12 | companion object {
13 | fun create(repoUrl: String) = NetworkCompat.createApi(repoUrl)
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/model/RepositoryMenu.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class RepositoryMenu(
7 | val option: Option = Option.Name,
8 | val descending: Boolean = false,
9 | val pinInstalled: Boolean = true,
10 | val pinUpdatable: Boolean = true,
11 | val showIcon: Boolean = true,
12 | val showLicense: Boolean = true,
13 | val showUpdatedTime: Boolean = false
14 | )
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/utils/timber/DebugTree.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.utils.timber
2 |
3 | import timber.log.Timber
4 |
5 | class DebugTree : Timber.DebugTree() {
6 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
7 | super.log(priority, "$tag", message, t)
8 | }
9 |
10 | override fun createStackElementTag(element: StackTraceElement): String {
11 | return super.createStackElementTag(element) + "(L${element.lineNumber})"
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/model/online/ModulesJson.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.model.online
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class ModulesJson(
7 | val name: String,
8 | val timestamp: Long,
9 | val metadata: Metadata = Metadata(),
10 | val modules: List
11 | ) {
12 | @Serializable
13 | data class Metadata(
14 | val homepage: String = "",
15 | val donate: String = "",
16 | val support: String = ""
17 | )
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/brand_open_source.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/model/online/VersionItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.model.online
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.Transient
6 |
7 | @Serializable
8 | data class VersionItem(
9 | @Transient
10 | val repoUrl: String = "",
11 | val timestamp: Long,
12 | val version: String,
13 | @SerialName("version_code")
14 | val versionCode: Int,
15 | @SerialName("zip_url")
16 | val zipUrl: String,
17 | val changelog: String
18 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/command.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/utils/extensions/ParcelableExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.utils.extensions
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.os.Parcelable
6 | import androidx.core.content.IntentCompat
7 | import androidx.core.os.BundleCompat
8 |
9 | inline fun Intent.parcelable(
10 | key: String
11 | ): T? = IntentCompat.getParcelableExtra(this, key, T::class.java)
12 |
13 | inline fun Bundle.parcelable(
14 | key: String
15 | ): T? = BundleCompat.getParcelable(this, key, T::class.java)
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | val Typography = Typography(
10 | titleLarge = TextStyle(
11 | fontFamily = FontFamily.SansSerif,
12 | fontWeight = FontWeight.Medium,
13 | fontSize = 20.sp,
14 | lineHeight = 28.sp,
15 | letterSpacing = 0.sp,
16 | )
17 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/utils/MenuExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.utils
2 |
3 | import androidx.compose.foundation.shape.CornerBasedShape
4 | import androidx.compose.foundation.shape.RoundedCornerShape
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.unit.dp
8 |
9 | @Composable
10 | fun ProvideMenuShape(
11 | value: CornerBasedShape = RoundedCornerShape(8.dp),
12 | content: @Composable () -> Unit
13 | ) = MaterialTheme(
14 | shapes = MaterialTheme.shapes.copy(extraSmall = value),
15 | content = content
16 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/search.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/app/Const.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.app
2 |
3 | import android.os.Environment
4 | import java.io.File
5 |
6 | object Const {
7 | val PUBLIC_DOWNLOADS: File = Environment.getExternalStoragePublicDirectory(
8 | Environment.DIRECTORY_DOWNLOADS
9 | )
10 |
11 | const val MY_GITHUB_URL = "https://github.com/SanmerDev"
12 | const val TRANSLATE_URL = "https://weblate.sanmer.app/engage/mrepo"
13 | const val GITHUB_URL = "https://github.com/MRepoApp/MRepo"
14 | const val TELEGRAM_URL = "https://t.me/mrepo_news"
15 | const val DEMO_REPO_URL = "https://demo-repo.sanmer.app/"
16 | const val SPDX_URL = "https://spdx.org/licenses/%s.json"
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloud.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/refresh.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/reload.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/currency_dollar.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/notification.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circle_check_filled.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | dependencyResolutionManagement {
5 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
6 | repositories {
7 | google()
8 | mavenCentral()
9 | maven("https://jitpack.io")
10 |
11 | mavenLocal {
12 | content {
13 | includeGroup("dev.sanmer.su")
14 | }
15 | }
16 | }
17 | }
18 |
19 | pluginManagement {
20 | includeBuild("build-logic")
21 | repositories {
22 | google()
23 | mavenCentral()
24 | gradlePluginPortal()
25 | }
26 | }
27 |
28 | rootProject.name = "MRepo"
29 | include(":core", ":app")
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/utils/StrUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.utils
2 |
3 | object StrUtil {
4 | fun getVersionDisplay(version: String, versionCode: Int): String {
5 | val included = "\\(.*?${versionCode}.*?\\)".toRegex()
6 | .containsMatchIn(version)
7 |
8 | return if (included) {
9 | version
10 | } else {
11 | "$version (${versionCode})"
12 | }
13 | }
14 |
15 | fun getFilename(name: String, version: String, versionCode: Int, extension: String): String {
16 | val versionNew = version.replace("\\([^)]*\\)".toRegex(), "")
17 | return "${name}-${versionNew}-${versionCode}.${extension}"
18 | .replace("[\\\\/:*?\"<>|]".toRegex(), "")
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/sun.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/compat/BuildCompat.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.compat
2 |
3 | import android.os.Build
4 | import androidx.annotation.ChecksSdkIntAtLeast
5 |
6 | object BuildCompat {
7 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
8 | val atLeastT get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
9 |
10 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
11 | val atLeastS get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
12 |
13 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
14 | val atLeastR get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
15 |
16 | @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
17 | val atLeastP get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/alert_circle_filled.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/circle_x_filled.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/brightness_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/App.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 | import dev.sanmer.mrepo.app.utils.NotificationUtils
6 | import dev.sanmer.mrepo.compat.NetworkCompat
7 | import dev.sanmer.mrepo.utils.timber.DebugTree
8 | import dev.sanmer.mrepo.utils.timber.ReleaseTree
9 | import timber.log.Timber
10 |
11 | @HiltAndroidApp
12 | class App : Application() {
13 | init {
14 | if (BuildConfig.DEBUG) {
15 | Timber.plant(DebugTree())
16 | } else {
17 | Timber.plant(ReleaseTree())
18 | }
19 | }
20 |
21 | override fun onCreate() {
22 | super.onCreate()
23 |
24 | NotificationUtils.init(this)
25 | NetworkCompat.setCacheDir(cacheDir)
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/navigation/Main.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.navigation
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.annotation.StringRes
5 | import dev.sanmer.mrepo.R
6 |
7 | enum class MainScreen(
8 | val route: String,
9 | @StringRes val label: Int,
10 | @DrawableRes val icon: Int
11 | ) {
12 | Repository(
13 | route = "RepositoryScreen",
14 | label = R.string.page_repository,
15 | icon = R.drawable.cloud
16 | ),
17 |
18 | Modules(
19 | route = "ModulesScreen",
20 | label = R.string.page_modules,
21 | icon = R.drawable.hexagons
22 | ),
23 |
24 | Settings(
25 | route = "SettingsScreen",
26 | label = R.string.page_settings,
27 | icon = R.drawable.settings
28 | )
29 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/utils/extensions/LocalDateTimeExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.utils.extensions
2 |
3 | import kotlinx.datetime.Clock
4 | import kotlinx.datetime.Instant
5 | import kotlinx.datetime.LocalDate
6 | import kotlinx.datetime.LocalDateTime
7 | import kotlinx.datetime.TimeZone
8 | import kotlinx.datetime.toLocalDateTime
9 |
10 | fun Long.toDateTime(): LocalDateTime {
11 | val instant = Instant.fromEpochMilliseconds(this)
12 | return instant.toLocalDateTime(TimeZone.currentSystemDefault())
13 | }
14 |
15 | fun Long.toDate(): LocalDate {
16 | val instant = Instant.fromEpochMilliseconds(this)
17 | return instant.toLocalDateTime(TimeZone.currentSystemDefault()).date
18 | }
19 |
20 | fun LocalDateTime.Companion.now() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/utils/NavControllerExt.kt:
--------------------------------------------------------------------------------
1 | package dev.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 | ) = navigate(
11 | route = route
12 | ) {
13 | launchSingleTop = true
14 | restoreState = true
15 | builder()
16 | }
17 |
18 | fun NavController.navigatePopUpTo(
19 | route: String
20 | ) = navigateSingleTopTo(
21 | route = route
22 | ) {
23 | popUpTo(
24 | id = currentDestination?.parent?.id ?: graph.findStartDestination().id
25 | ) {
26 | saveState = true
27 | inclusive = true
28 | }
29 | }
--------------------------------------------------------------------------------
/core/src/main/aidl/dev/sanmer/mrepo/stub/IModuleManager.aidl:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.stub;
2 |
3 | import dev.sanmer.mrepo.content.Module;
4 | import dev.sanmer.mrepo.stub.IInstallCallback;
5 | import dev.sanmer.mrepo.stub.IModuleOpsCallback;
6 |
7 | interface IModuleManager {
8 | String getVersion();
9 | int getVersionCode();
10 | String getPlatform();
11 |
12 | List getModules();
13 | Module getModuleById(String id);
14 | Module getModuleInfo(String path);
15 |
16 | oneway void enable(String id, IModuleOpsCallback callback);
17 | oneway void disable(String id, IModuleOpsCallback callback);
18 | oneway void remove(String id, IModuleOpsCallback callback);
19 | oneway void install(String path, IInstallCallback callback);
20 |
21 | boolean deleteOnExit(String path);
22 | oneway void reboot();
23 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | time: "21:00"
8 | labels: [ "github_actions" ]
9 |
10 | - package-ecosystem: gradle
11 | directory: "/"
12 | schedule:
13 | interval: daily
14 | time: "21:00"
15 | labels: [ "dependencies" ]
16 | registries: "*"
17 | ignore:
18 | - dependency-name: "self.*"
19 | groups:
20 | kotlin-ksp:
21 | patterns:
22 | - "org.jetbrains.kotlin:*"
23 | - "org.jetbrains.kotlin.jvm"
24 | - "com.google.devtools.ksp"
25 | - "com.google.devtools.ksp.gradle.plugin"
26 |
27 | registries:
28 | maven-google:
29 | type: "maven-repository"
30 | url: "https://maven.google.com"
31 | replaces-base: true
32 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/menu_2.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/utils/extensions/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.utils.extensions
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import androidx.core.app.LocaleManagerCompat
6 | import androidx.core.app.ShareCompat
7 |
8 | val Context.tmpDir get() = cacheDir.resolve("tmp")
9 | .apply {
10 | if (!exists()) mkdirs()
11 | }
12 |
13 | val Context.applicationLocale
14 | get() = LocaleManagerCompat.getApplicationLocales(applicationContext)
15 | .toList().firstOrNull()
16 |
17 | fun Context.openUrl(url: String) {
18 | startActivity(
19 | Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
20 | )
21 | }
22 |
23 | fun Context.shareText(text: String) {
24 | ShareCompat.IntentBuilder(this)
25 | .setType("text/plain")
26 | .setText(text)
27 | .startChooser()
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_left.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/database/di/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.database.di
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import dev.sanmer.mrepo.database.AppDatabase
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | object DatabaseModule {
15 | @Provides
16 | @Singleton
17 | fun providesAppDatabase(
18 | @ApplicationContext context: Context
19 | ) = AppDatabase.build(context)
20 |
21 | @Provides
22 | @Singleton
23 | fun providesRepoDao(db: AppDatabase) = db.repoDao()
24 |
25 | @Provides
26 | @Singleton
27 | fun providesLocalDao(db: AppDatabase) = db.localDao()
28 | }
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/mrepo/ModuleManager.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo
2 |
3 | import android.os.IBinder
4 | import dev.sanmer.mrepo.impl.APatchModuleManagerImpl
5 | import dev.sanmer.mrepo.impl.KernelSUModuleManagerImpl
6 | import dev.sanmer.mrepo.impl.MagiskModuleManagerImpl
7 | import dev.sanmer.mrepo.impl.Shell.exec
8 | import dev.sanmer.su.IService
9 | import dev.sanmer.su.IServiceManager
10 |
11 | class ModuleManager : IService {
12 | override val name = "module"
13 |
14 | override fun create(manager: IServiceManager): IBinder = when {
15 | "which magisk".exec().isSuccess -> MagiskModuleManagerImpl()
16 | "which ksud".exec().isSuccess -> KernelSUModuleManagerImpl()
17 | "which apd".exec().isSuccess -> APatchModuleManagerImpl()
18 | else -> throw IllegalStateException("Unsupported platform (${manager.seLinuxContext})")
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/hexagons.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/info_circle_filled.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/cloud_download.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/home.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/app/utils/NotificationUtils.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.app.utils
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.content.Context
6 | import androidx.core.app.NotificationManagerCompat
7 | import dev.sanmer.mrepo.R
8 |
9 | object NotificationUtils {
10 | const val CHANNEL_ID_DOWNLOAD = "DOWNLOAD"
11 | const val NOTIFICATION_ID_DOWNLOAD = 1024
12 |
13 | fun init(context: Context) {
14 | val channels = listOf(
15 | NotificationChannel(CHANNEL_ID_DOWNLOAD,
16 | context.getString(R.string.notification_name_download),
17 | NotificationManager.IMPORTANCE_HIGH
18 | )
19 | )
20 |
21 | NotificationManagerCompat.from(context).apply {
22 | createNotificationChannels(channels)
23 | deleteUnlistedNotificationChannels(channels.map { it.id })
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/UserPreferencesSerializer.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore
2 |
3 | import androidx.datastore.core.CorruptionException
4 | import androidx.datastore.core.Serializer
5 | import dev.sanmer.mrepo.datastore.model.UserPreferences
6 | import kotlinx.serialization.SerializationException
7 | import java.io.InputStream
8 | import java.io.OutputStream
9 | import javax.inject.Inject
10 |
11 | class UserPreferencesSerializer @Inject constructor() : Serializer {
12 | override val defaultValue = UserPreferences()
13 |
14 | override suspend fun readFrom(input: InputStream) =
15 | try {
16 | UserPreferences.decodeFrom(input)
17 | } catch (e: SerializationException) {
18 | throw CorruptionException("Failed to read proto", e)
19 | }
20 |
21 | override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
22 | t.encodeTo(output)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/device_floppy.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/notification_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/award.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/alert_triangle.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/moon_stars.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/files.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings_untranslatable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | MRepo
4 | @string/module_install
5 |
6 |
7 | Shizuku
8 |
9 |
10 |
11 | @string/repo_update_at
12 |
13 |
14 |
15 | @string/setup_non_root_title
16 |
17 | @string/repository_empty
18 |
19 | GitHub
20 | Weblate
21 | Telegram
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/navigation/graphs/Modules.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.navigation.graphs
2 |
3 | import androidx.compose.animation.fadeIn
4 | import androidx.compose.animation.fadeOut
5 | import androidx.navigation.NavController
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.compose.composable
8 | import androidx.navigation.navigation
9 | import dev.sanmer.mrepo.ui.navigation.MainScreen
10 | import dev.sanmer.mrepo.ui.screens.modules.ModulesScreen
11 |
12 | enum class ModulesScreen(val route: String) {
13 | Home("Modules"),
14 | }
15 |
16 | fun NavGraphBuilder.modulesScreen(
17 | navController: NavController
18 | ) = navigation(
19 | startDestination = ModulesScreen.Home.route,
20 | route = MainScreen.Modules.route
21 | ) {
22 | composable(
23 | route = ModulesScreen.Home.route,
24 | enterTransition = { fadeIn() },
25 | exitTransition = { fadeOut() }
26 | ) {
27 | ModulesScreen(
28 | navController = navController
29 | )
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
16 |
17 |
23 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/screens/repository/view/items/TagItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.screens.repository.view.items
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.layout.size
5 | import androidx.compose.material3.FilledTonalIconButton
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.IconButtonDefaults
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.painterResource
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | internal fun TagItem(
16 | @DrawableRes icon: Int,
17 | onClick: () -> Unit
18 | ) = FilledTonalIconButton(
19 | onClick = onClick,
20 | colors = IconButtonDefaults.filledTonalIconButtonColors(
21 | containerColor = MaterialTheme.colorScheme.surfaceVariant
22 | ),
23 | modifier = Modifier.size(35.dp),
24 | ) {
25 | Icon(
26 | painter = painterResource(id = icon),
27 | contentDescription = null
28 | )
29 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/box.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/pencil_plus.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/telegram.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/di/DataStoreModule.kt:
--------------------------------------------------------------------------------
1 | package dev.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 dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import dev.sanmer.mrepo.datastore.UserPreferencesSerializer
13 | import dev.sanmer.mrepo.datastore.model.UserPreferences
14 | import javax.inject.Singleton
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | object DataStoreModule {
19 | @Provides
20 | @Singleton
21 | fun providesUserPreferencesDataStore(
22 | @ApplicationContext context: Context,
23 | userPreferencesSerializer: UserPreferencesSerializer
24 | ): DataStore =
25 | DataStoreFactory.create(
26 | serializer = userPreferencesSerializer
27 | ) {
28 | context.dataStoreFile("user_preferences.pb")
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/users.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/file_download.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/file_certificate.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/database/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.database
2 |
3 | import android.content.Context
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 | import dev.sanmer.mrepo.database.dao.LocalDao
8 | import dev.sanmer.mrepo.database.dao.RepoDao
9 | import dev.sanmer.mrepo.database.entity.local.LocalModuleEntity
10 | import dev.sanmer.mrepo.database.entity.online.OnlineModuleEntity
11 | import dev.sanmer.mrepo.database.entity.online.RepoEntity
12 | import dev.sanmer.mrepo.database.entity.online.VersionItemEntity
13 |
14 | @Database(
15 | entities = [
16 | RepoEntity::class,
17 | OnlineModuleEntity::class,
18 | VersionItemEntity::class,
19 | LocalModuleEntity::class,
20 | LocalModuleEntity.Updatable::class
21 | ],
22 | version = 1
23 | )
24 | abstract class AppDatabase : RoomDatabase() {
25 | abstract fun repoDao(): RepoDao
26 | abstract fun localDao(): LocalDao
27 |
28 | companion object {
29 | fun build(context: Context) =
30 | Room.databaseBuilder(context,
31 | AppDatabase::class.java, "mrepo")
32 | .build()
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/color_swatch.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/utils/LazyListStateExt.kt:
--------------------------------------------------------------------------------
1 | package dev.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.mutableIntStateOf
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) { mutableIntStateOf(firstVisibleItemIndex) }
15 | var previousScrollOffset by remember(this) { mutableIntStateOf(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 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/fr.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest an idea for this project
3 | labels: [ "enhancement" ]
4 | title: "[FR] "
5 | body:
6 | - type: checkboxes
7 | id: checklist
8 | attributes:
9 | label: Checklist
10 | description: Ensure that our bug report form is appropriate for you
11 | options:
12 | - label: No one has submitted a similar or identical feature request before
13 | required: true
14 | - label: This suggestion does not depart from the original intention of MRepo
15 | required: true
16 | - type: textarea
17 | id: propose
18 | attributes:
19 | label: Enhancement propose
20 | description: Propose of the enhancement
21 | placeholder: |
22 | Show your idea here
23 | validations:
24 | required: true
25 | - type: textarea
26 | id: solution
27 | attributes:
28 | label: Solution
29 | description: What's your solution for this enhancement
30 | placeholder: |
31 | How to do it on your opinion
32 | - type: textarea
33 | id: addition
34 | attributes:
35 | label: Additional info
36 | description: Everything else you consider worthy that we didn't ask for
--------------------------------------------------------------------------------
/app/src/main/res/drawable/heart_handshake.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/launcher_outline.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/trash.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/file_type_zip.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/device_mobile_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/share.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/database/entity/online/RepoEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.database.entity.online
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Entity
5 | import dev.sanmer.mrepo.model.online.ModulesJson
6 |
7 | @Entity(
8 | tableName = "repo",
9 | primaryKeys = ["url"]
10 | )
11 | data class RepoEntity(
12 | val url: String,
13 | val disable: Boolean,
14 | val size: Int,
15 | val name: String,
16 | val timestamp: Long,
17 | @Embedded val metadata: Metadata
18 | ) {
19 | constructor(url: String) : this(
20 | url = url,
21 | disable = false,
22 | size = 0,
23 | name = url,
24 | timestamp = 0L,
25 | metadata = Metadata()
26 | )
27 |
28 | fun copy(modulesJson: ModulesJson) = copy(
29 | size = modulesJson.modules.size,
30 | name = modulesJson.name,
31 | timestamp = modulesJson.timestamp,
32 | metadata = Metadata(modulesJson.metadata)
33 | )
34 |
35 | data class Metadata(
36 | val homepage: String = "",
37 | val donate: String = "",
38 | val support: String = ""
39 | ) {
40 | constructor(original: ModulesJson.Metadata) : this(
41 | homepage = original.homepage,
42 | donate = original.donate,
43 | support = original.support
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/database/entity/online/VersionItemEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.database.entity.online
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import dev.sanmer.mrepo.model.online.VersionItem
6 |
7 | @Entity(
8 | tableName = "version",
9 | primaryKeys = ["id", "repo_url", "version_code"]
10 | )
11 | data class VersionItemEntity(
12 | @ColumnInfo(name = "repo_url")
13 | val repoUrl: String,
14 | val id: String,
15 | val timestamp: Long,
16 | val version: String,
17 | @ColumnInfo(name = "version_code")
18 | val versionCode: Int,
19 | @ColumnInfo(name = "zip_url")
20 | val zipUrl: String,
21 | val changelog: String
22 | ) {
23 | constructor(
24 | repoUrl: String,
25 | id: String,
26 | original: VersionItem,
27 | ) : this(
28 | repoUrl = repoUrl,
29 | id = id,
30 | timestamp = original.timestamp,
31 | version = original.version,
32 | versionCode = original.versionCode,
33 | zipUrl = original.zipUrl,
34 | changelog = original.changelog
35 | )
36 |
37 | fun toJson() = VersionItem(
38 | repoUrl = repoUrl,
39 | timestamp = timestamp,
40 | version = version,
41 | versionCode = versionCode,
42 | zipUrl = zipUrl,
43 | changelog = changelog
44 | )
45 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/world.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/model/online/OnlineModule.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.model.online
2 |
3 | import dev.sanmer.mrepo.utils.StrUtil
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class OnlineModule(
9 | val id: String,
10 | val name: String,
11 | val version: String,
12 | @SerialName("version_code")
13 | val versionCode: Int,
14 | val author: String,
15 | val description: String,
16 | val metadata: Metadata = Metadata(),
17 | val versions: List,
18 | ) {
19 | val versionDisplay by lazy {
20 | StrUtil.getVersionDisplay(version, versionCode)
21 | }
22 |
23 | @Serializable
24 | data class Metadata(
25 | val license: String = "",
26 | val homepage: String = "",
27 | val source: String = "",
28 | val donate: String = "",
29 | val support: String = ""
30 | )
31 |
32 | companion object {
33 | fun example() = OnlineModule(
34 | id = "online_example",
35 | name = "Example",
36 | version = "2022.08.16",
37 | versionCode = 1703,
38 | author = "Sanmer",
39 | description = "This is an example!",
40 | metadata = Metadata(
41 | license = "GPL-3.0"
42 | ),
43 | versions = emptyList()
44 | )
45 | }
46 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MRepo
2 | [](https://github.com/MRepoApp/MRepo/releases) [](https://github.com/MRepoApp/MRepo/releases/latest)
3 |
4 | MRepo is an Android app that helps manage your own modules repository.
5 |
6 | ## Preview
7 |
8 |

9 |
10 | ## Supported Versions
11 | - Android 8.0 ~ 14
12 | - Magisk 24.0 ~ latest
13 | - KernelSU 0.5.1 ~ latest
14 | - APatch 10253 ~ latest
15 |
16 | ## Modules Repository
17 | - [mrepo-rs](https://github.com/MRepoApp/mrepo-rs): A manager for building modules repository
18 | - [demo-modules-repo](https://github.com/MRepoApp/demo-modules-repo): A demo of Magisk Modules Repo
19 | - [magisk-modules-alt-repo](https://github.com/MRepoApp/magisk-modules-alt-repo): A mirror of Magisk-Modules-Alt-Repo
20 |
21 | ## Credits
22 | - [tabler/tabler-icons](https://github.com/tabler/tabler-icons.git)
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/package_import.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/component/Logo.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.component
2 |
3 | import androidx.annotation.DrawableRes
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.shape.CircleShape
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.LocalContentColor
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Surface
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.Shape
16 | import androidx.compose.ui.res.painterResource
17 |
18 | @Composable
19 | fun Logo(
20 | @DrawableRes icon: Int,
21 | modifier: Modifier = Modifier,
22 | shape: Shape = CircleShape,
23 | contentColor: Color = MaterialTheme.colorScheme.onPrimary,
24 | containerColor: Color = MaterialTheme.colorScheme.primary,
25 | fraction: Float = 0.6f
26 | ) = Surface(
27 | modifier = modifier,
28 | shape = shape,
29 | color = containerColor,
30 | contentColor = contentColor
31 | ) {
32 | Box(
33 | contentAlignment = Alignment.Center
34 | ) {
35 | Icon(
36 | modifier = Modifier.fillMaxSize(fraction),
37 | painter = painterResource(id = icon),
38 | contentDescription = null,
39 | tint = LocalContentColor.current
40 | )
41 | }
42 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/repository/UserPreferencesRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.repository
2 |
3 | import dev.sanmer.mrepo.datastore.UserPreferencesDataSource
4 | import dev.sanmer.mrepo.datastore.model.DarkMode
5 | import dev.sanmer.mrepo.datastore.model.Homepage
6 | import dev.sanmer.mrepo.datastore.model.ModulesMenu
7 | import dev.sanmer.mrepo.datastore.model.RepositoryMenu
8 | import dev.sanmer.mrepo.datastore.model.WorkingMode
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class UserPreferencesRepository @Inject constructor(
14 | private val userPreferencesDataSource: UserPreferencesDataSource
15 | ) {
16 | val data get() = userPreferencesDataSource.data
17 |
18 | suspend fun setWorkingMode(value: WorkingMode) = userPreferencesDataSource.setWorkingMode(value)
19 |
20 | suspend fun setDarkTheme(value: DarkMode) = userPreferencesDataSource.setDarkTheme(value)
21 |
22 | suspend fun setThemeColor(value: Int) = userPreferencesDataSource.setThemeColor(value)
23 |
24 | suspend fun setDeleteZipFile(value: Boolean) = userPreferencesDataSource.setDeleteZipFile(value)
25 |
26 | suspend fun setDownloadPath(value: String) = userPreferencesDataSource.setDownloadPath(value)
27 |
28 | suspend fun setHomepage(value: Homepage) = userPreferencesDataSource.setHomepage(value)
29 |
30 | suspend fun setRepositoryMenu(value: RepositoryMenu) = userPreferencesDataSource.setRepositoryMenu(value)
31 |
32 | suspend fun setModulesMenu(value: ModulesMenu) = userPreferencesDataSource.setModulesMenu(value)
33 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/git_pull_request.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/navigation/graphs/Repository.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.navigation.graphs
2 |
3 | import androidx.compose.animation.fadeIn
4 | import androidx.compose.animation.fadeOut
5 | import androidx.navigation.NavController
6 | import androidx.navigation.NavGraphBuilder
7 | import androidx.navigation.NavType
8 | import androidx.navigation.compose.composable
9 | import androidx.navigation.navArgument
10 | import androidx.navigation.navigation
11 | import dev.sanmer.mrepo.ui.navigation.MainScreen
12 | import dev.sanmer.mrepo.ui.screens.repository.RepositoryScreen
13 | import dev.sanmer.mrepo.ui.screens.repository.view.ViewScreen
14 |
15 | enum class RepositoryScreen(val route: String) {
16 | Home("Repository"),
17 | View("View/{moduleId}")
18 | }
19 |
20 | fun NavGraphBuilder.repositoryScreen(
21 | navController: NavController
22 | ) = navigation(
23 | startDestination = RepositoryScreen.Home.route,
24 | route = MainScreen.Repository.route
25 | ) {
26 | composable(
27 | route = RepositoryScreen.Home.route,
28 | enterTransition = { fadeIn() },
29 | exitTransition = { fadeOut() }
30 | ) {
31 | RepositoryScreen(
32 | navController = navController
33 | )
34 | }
35 |
36 | composable(
37 | route = RepositoryScreen.View.route,
38 | arguments = listOf(navArgument("moduleId") { type = NavType.StringType }),
39 | enterTransition = { fadeIn() },
40 | exitTransition = { fadeOut() }
41 | ) {
42 | ViewScreen(
43 | navController = navController
44 | )
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/component/AppBar.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.component
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.LocalContentColor
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.painterResource
15 | import androidx.compose.ui.text.style.TextOverflow
16 | import androidx.compose.ui.unit.dp
17 | import dev.sanmer.mrepo.BuildConfig
18 | import dev.sanmer.mrepo.R
19 |
20 | @Composable
21 | fun TopAppBarTitle(
22 | text: String,
23 | modifier: Modifier = Modifier
24 | ) = Row(
25 | modifier = modifier,
26 | horizontalArrangement = Arrangement.Start,
27 | verticalAlignment = Alignment.CenterVertically
28 | ) {
29 | Text(
30 | text = text,
31 | style = MaterialTheme.typography.titleLarge,
32 | maxLines = 1,
33 | overflow = TextOverflow.Ellipsis,
34 | color = LocalContentColor.current
35 | )
36 |
37 | if (BuildConfig.IS_DEV_VERSION) {
38 | Spacer(modifier = Modifier.width(10.dp))
39 | Icon(
40 | painter = painterResource(id = R.drawable.ci_label),
41 | contentDescription = null,
42 | tint = LocalContentColor.current
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/mrepo/impl/Shell.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.impl
2 |
3 | import android.util.Log
4 |
5 | internal object Shell {
6 | private const val TAG = "Shell"
7 |
8 | fun String.exec(): Result =
9 | runCatching {
10 | Log.d(TAG, "exec: $this")
11 | val process = ProcessBuilder("sh", "-c", this).start()
12 | val output = process.inputStream.bufferedReader().readText()
13 | .removeSurrounding("", "\n")
14 |
15 | val error = process.errorStream.bufferedReader().readText()
16 | .removeSurrounding("", "\n")
17 |
18 | require(process.waitFor().ok()) { error }
19 | Log.d(TAG, "output: $output")
20 |
21 | output
22 | }.onFailure {
23 | Log.e(TAG, Log.getStackTraceString(it))
24 | }
25 |
26 | fun String.exec(
27 | stdout: (String) -> Unit,
28 | stderr: (String) -> Unit
29 | ) = runCatching {
30 | Log.d(TAG, "exec: $this")
31 | val process = ProcessBuilder("sh", "-c", this).start()
32 | val output = process.inputStream.bufferedReader()
33 | val error = process.errorStream.bufferedReader()
34 |
35 | output.forEachLine {
36 | Log.d(TAG, "output: $it")
37 | stdout(it)
38 | }
39 |
40 | error.forEachLine {
41 | Log.d(TAG, "error: $it")
42 | stderr(it)
43 | }
44 |
45 | require(process.waitFor().ok())
46 | }.onFailure {
47 | Log.e(TAG, Log.getStackTraceString(it))
48 | }
49 |
50 | private fun Int.ok() = this == 0
51 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
16 |
22 |
28 |
34 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/model/json/UpdateJson.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.model.json
2 |
3 | import dev.sanmer.mrepo.compat.NetworkCompat
4 | import dev.sanmer.mrepo.model.online.VersionItem
5 | import dev.sanmer.mrepo.utils.StrUtil
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.json.decodeFromStream
8 |
9 | @Serializable
10 | data class UpdateJson(
11 | val version: String,
12 | val versionCode: Int,
13 | val zipUrl: String,
14 | val changelog: String
15 | ) {
16 | fun toItemOrNull(timestamp: Long): VersionItem? {
17 | if (!NetworkCompat.isUrl(zipUrl)) return null
18 |
19 | val changelog = when {
20 | !NetworkCompat.isUrl(changelog) -> ""
21 | NetworkCompat.isBlobUrl(changelog) -> ""
22 | else -> changelog
23 | }
24 |
25 | return VersionItem(
26 | timestamp = timestamp,
27 | version = StrUtil.getVersionDisplay(version, versionCode),
28 | versionCode = versionCode,
29 | zipUrl = zipUrl,
30 | changelog = changelog
31 | )
32 | }
33 |
34 | companion object {
35 | suspend fun load(url: String): VersionItem? {
36 | if (!NetworkCompat.isUrl(url)) return null
37 |
38 | val result = NetworkCompat.request(url) { body, headers ->
39 | val json = NetworkCompat.defaultJson.decodeFromStream(body.byteStream())
40 | val lastModified = headers.getInstant("Last-Modified")?.toEpochMilli()
41 |
42 | json.toItemOrNull(lastModified ?: System.currentTimeMillis())
43 | }
44 |
45 | return result.getOrNull()
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/component/LabelItem.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.component
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.shape.RoundedCornerShape
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.graphics.Color
13 | import androidx.compose.ui.graphics.Shape
14 | import androidx.compose.ui.text.intl.Locale
15 | import androidx.compose.ui.text.toUpperCase
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.unit.sp
18 |
19 | @Composable
20 | fun LabelItem(
21 | text: String,
22 | containerColor: Color = MaterialTheme.colorScheme.primary,
23 | contentColor: Color = MaterialTheme.colorScheme.onPrimary,
24 | shape: Shape = RoundedCornerShape(3.dp),
25 | upperCase: Boolean = true
26 | ) {
27 | if (text.isBlank()) return
28 |
29 | Box(
30 | modifier = Modifier
31 | .background(
32 | color = containerColor,
33 | shape = shape
34 | ),
35 | contentAlignment = Alignment.Center
36 | ) {
37 | Text(
38 | text = when {
39 | upperCase -> text.toUpperCase(Locale.current)
40 | else -> text
41 | },
42 | style = MaterialTheme.typography.labelSmall.copy(fontSize = 8.sp),
43 | color = contentColor,
44 | modifier = Modifier.padding(horizontal = 4.dp)
45 | )
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/database/entity/local/LocalModuleEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.database.entity.local
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import dev.sanmer.mrepo.model.local.LocalModule
6 | import dev.sanmer.mrepo.model.local.State
7 |
8 | @Entity(
9 | tableName = "local",
10 | primaryKeys = ["id"]
11 | )
12 | data class LocalModuleEntity(
13 | val id: String,
14 | val name: String,
15 | val version: String,
16 | @ColumnInfo(name = "version_code")
17 | val versionCode: Int,
18 | val author: String,
19 | val description: String,
20 | val state: String,
21 | @ColumnInfo(name = "update_json")
22 | val updateJson: String,
23 | @ColumnInfo(name = "last_updated")
24 | val lastUpdated: Long
25 | ) {
26 | constructor(original: LocalModule) : this(
27 | id = original.id,
28 | name = original.name,
29 | version = original.version,
30 | versionCode = original.versionCode,
31 | author = original.author,
32 | description = original.description,
33 | state = original.state.name,
34 | updateJson = original.updateJson,
35 | lastUpdated = original.lastUpdated
36 | )
37 |
38 | fun toModule() = LocalModule(
39 | id = id,
40 | name = name,
41 | version = version,
42 | versionCode = versionCode,
43 | author = author,
44 | description = description,
45 | updateJson = updateJson,
46 | state = State.valueOf(state),
47 | lastUpdated = lastUpdated
48 | )
49 |
50 | @Entity(
51 | tableName = "local_updatable",
52 | primaryKeys = ["id"]
53 | )
54 | data class Updatable(
55 | val id: String,
56 | val updatable: Boolean
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/screens/repository/ModulesList.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.screens.repository
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.LazyListState
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.dp
13 | import androidx.navigation.NavController
14 | import dev.sanmer.mrepo.ui.component.scrollbar.VerticalFastScrollbar
15 | import dev.sanmer.mrepo.ui.utils.navigateSingleTopTo
16 | import dev.sanmer.mrepo.viewmodel.ModuleViewModel
17 | import dev.sanmer.mrepo.viewmodel.RepositoryViewModel
18 |
19 | @Composable
20 | internal fun ModulesList(
21 | state: LazyListState,
22 | navController: NavController,
23 | list: List,
24 | ) = Box(
25 | modifier = Modifier.fillMaxSize()
26 | ) {
27 | LazyColumn(
28 | state = state,
29 | modifier = Modifier.fillMaxSize(),
30 | verticalArrangement = Arrangement.spacedBy(5.dp)
31 | ) {
32 | items(
33 | items = list,
34 | key = { it.original.id }
35 | ) { module ->
36 | ModuleItem(
37 | module = module,
38 | onClick = {
39 | navController.navigateSingleTopTo(
40 | ModuleViewModel.putModuleId(module.original)
41 | )
42 | }
43 | )
44 | }
45 | }
46 |
47 | VerticalFastScrollbar(
48 | state = state,
49 | modifier = Modifier.align(Alignment.CenterEnd)
50 | )
51 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/viewmodel/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import dev.sanmer.mrepo.Compat
7 | import dev.sanmer.mrepo.datastore.model.DarkMode
8 | import dev.sanmer.mrepo.datastore.model.WorkingMode
9 | import dev.sanmer.mrepo.repository.UserPreferencesRepository
10 | import kotlinx.coroutines.launch
11 | import timber.log.Timber
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class SettingsViewModel @Inject constructor(
16 | private val userPreferencesRepository: UserPreferencesRepository
17 | ) : ViewModel() {
18 | private val mm get() = Compat.moduleManager
19 | val isProviderAlive get() = Compat.isAlive
20 |
21 | val version get() = Compat.get("") {
22 | with(mm) { "$version (${versionCode})" }
23 | }
24 |
25 | init {
26 | Timber.d("SettingsViewModel init")
27 | }
28 |
29 | fun setWorkingMode(value: WorkingMode) {
30 | viewModelScope.launch {
31 | userPreferencesRepository.setWorkingMode(value)
32 | }
33 | }
34 |
35 | fun setDarkTheme(value: DarkMode) {
36 | viewModelScope.launch {
37 | userPreferencesRepository.setDarkTheme(value)
38 | }
39 | }
40 |
41 | fun setThemeColor(value: Int) {
42 | viewModelScope.launch {
43 | userPreferencesRepository.setThemeColor(value)
44 | }
45 | }
46 |
47 | fun setDeleteZipFile(value: Boolean) {
48 | viewModelScope.launch {
49 | userPreferencesRepository.setDeleteZipFile(value)
50 | }
51 | }
52 |
53 | fun setDownloadPath(value: String) {
54 | viewModelScope.launch {
55 | userPreferencesRepository.setDownloadPath(value)
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/brand_git.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
24 |
30 |
36 |
42 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/repository/ModulesRepository.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.repository
2 |
3 | import dev.sanmer.mrepo.Compat
4 | import dev.sanmer.mrepo.compat.NetworkCompat.runRequest
5 | import dev.sanmer.mrepo.database.entity.online.RepoEntity
6 | import dev.sanmer.mrepo.stub.IRepoManager
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 | import timber.log.Timber
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class ModulesRepository @Inject constructor(
15 | private val localRepository: LocalRepository,
16 | ) {
17 | private val mm get() = Compat.moduleManager
18 |
19 | suspend fun getLocalAll() = withContext(Dispatchers.IO) {
20 | runCatching {
21 | mm.modules.toList()
22 | }.onSuccess { modules ->
23 | localRepository.updateLocal(modules)
24 | }.onFailure {
25 | Timber.e(it, "getLocalAll")
26 | }
27 | }
28 |
29 | suspend fun getLocal(id: String) = withContext(Dispatchers.IO) {
30 | runCatching {
31 | mm.getModuleById(id)
32 | }.onSuccess {
33 | localRepository.insertLocal(it)
34 | }.onFailure {
35 | Timber.e(it, "getLocal: $id")
36 | }
37 | }
38 |
39 | suspend fun getRepoAll(onlyEnable: Boolean = true) =
40 | localRepository.getRepoAll().filter {
41 | if (onlyEnable) !it.disable else true
42 | }.map {
43 | getRepo(it)
44 | }
45 |
46 | suspend fun getRepo(repo: RepoEntity) =
47 | runRequest {
48 | val api = IRepoManager.create(repo.url)
49 | api.modules.execute()
50 | }.onSuccess {
51 | localRepository.updateRepo(repo, it)
52 | }.onFailure {
53 | Timber.e(it, "getRepo: ${repo.url}")
54 | }
55 |
56 | suspend fun getRepo(repoUrl: String) = getRepo(RepoEntity(repoUrl))
57 | }
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/mrepo/impl/APatchModuleManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.impl
2 |
3 | import dev.sanmer.mrepo.Platform
4 | import dev.sanmer.mrepo.impl.Shell.exec
5 | import dev.sanmer.mrepo.stub.IInstallCallback
6 | import dev.sanmer.mrepo.stub.IModuleOpsCallback
7 | import dev.sanmer.su.wrap.ThrowableWrapper.Companion.wrap
8 | import java.io.File
9 | import java.io.FileNotFoundException
10 |
11 | internal class APatchModuleManagerImpl : BaseModuleManagerImpl() {
12 | override fun getPlatform(): String {
13 | return Platform.APatch.name
14 | }
15 |
16 | override fun enable(id: String, callback: IModuleOpsCallback?) {
17 | moduleOps(
18 | cmd = "apd module enable $id",
19 | id = id,
20 | callback = callback
21 | )
22 | }
23 |
24 | override fun disable(id: String, callback: IModuleOpsCallback?) {
25 | moduleOps(
26 | cmd = "apd module disable $id",
27 | id = id,
28 | callback = callback
29 | )
30 | }
31 |
32 | override fun remove(id: String, callback: IModuleOpsCallback?) {
33 | moduleOps(
34 | cmd = "apd module uninstall $id",
35 | id = id,
36 | callback = callback
37 | )
38 | }
39 |
40 | override fun install(path: String, callback: IInstallCallback?) {
41 | install(
42 | cmd = "apd module install '${path}'",
43 | path = path,
44 | callback = callback
45 | )
46 | }
47 |
48 | private fun moduleOps(cmd: String, id: String, callback: IModuleOpsCallback?) {
49 | val moduleDir = File(modulesDir, id)
50 | if (!moduleDir.exists()) {
51 | callback?.onFailure(id, FileNotFoundException(moduleDir.path).wrap())
52 | return
53 | }
54 |
55 | cmd.exec().onSuccess {
56 | callback?.onSuccess(id)
57 | }.onFailure {
58 | callback?.onFailure(id, it.wrap())
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/core/src/main/kotlin/dev/sanmer/mrepo/impl/KernelSUModuleManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.impl
2 |
3 | import dev.sanmer.mrepo.Platform
4 | import dev.sanmer.mrepo.impl.Shell.exec
5 | import dev.sanmer.mrepo.stub.IInstallCallback
6 | import dev.sanmer.mrepo.stub.IModuleOpsCallback
7 | import dev.sanmer.su.wrap.ThrowableWrapper.Companion.wrap
8 | import java.io.File
9 | import java.io.FileNotFoundException
10 |
11 | internal class KernelSUModuleManagerImpl : BaseModuleManagerImpl() {
12 | override fun getPlatform(): String {
13 | return Platform.KernelSU.name
14 | }
15 |
16 | override fun enable(id: String, callback: IModuleOpsCallback?) {
17 | moduleOps(
18 | cmd = "ksud module enable $id",
19 | id = id,
20 | callback = callback
21 | )
22 | }
23 |
24 | override fun disable(id: String, callback: IModuleOpsCallback?) {
25 | moduleOps(
26 | cmd = "ksud module disable $id",
27 | id = id,
28 | callback = callback
29 | )
30 | }
31 |
32 | override fun remove(id: String, callback: IModuleOpsCallback?) {
33 | moduleOps(
34 | cmd = "ksud module uninstall $id",
35 | id = id,
36 | callback = callback
37 | )
38 | }
39 |
40 | override fun install(path: String, callback: IInstallCallback?) {
41 | install(
42 | cmd = "ksud module install '${path}'",
43 | path = path,
44 | callback = callback
45 | )
46 | }
47 |
48 | private fun moduleOps(cmd: String, id: String, callback: IModuleOpsCallback?) {
49 | val moduleDir = File(modulesDir, id)
50 | if (!moduleDir.exists()) {
51 | callback?.onFailure(id, FileNotFoundException(moduleDir.path).wrap())
52 | return
53 | }
54 |
55 | cmd.exec().onSuccess {
56 | callback?.onSuccess(id)
57 | }.onFailure {
58 | callback?.onFailure(id, it.wrap())
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/datastore/model/UserPreferences.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.datastore.model
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.runtime.Composable
5 | import dev.sanmer.mrepo.app.Const
6 | import dev.sanmer.mrepo.compat.BuildCompat
7 | import dev.sanmer.mrepo.datastore.model.WorkingMode.Companion.isNonRoot
8 | import dev.sanmer.mrepo.ui.theme.Colors
9 | import kotlinx.serialization.Serializable
10 | import kotlinx.serialization.decodeFromByteArray
11 | import kotlinx.serialization.encodeToByteArray
12 | import kotlinx.serialization.protobuf.ProtoBuf
13 | import kotlinx.serialization.protobuf.ProtoNumber
14 | import java.io.InputStream
15 | import java.io.OutputStream
16 |
17 | @Serializable
18 | data class UserPreferences(
19 | val workingMode: WorkingMode = WorkingMode.Setup,
20 | val darkMode: DarkMode = DarkMode.FollowSystem,
21 | val themeColor: Int = if (BuildCompat.atLeastS) Colors.Dynamic.id else Colors.Pourville.id,
22 | val deleteZipFile: Boolean = false,
23 | val downloadPath: String = Const.PUBLIC_DOWNLOADS.path,
24 | val homepage: Homepage = Homepage.Repository,
25 | @ProtoNumber(20)
26 | val repositoryMenu: RepositoryMenu = RepositoryMenu(),
27 | @ProtoNumber(30)
28 | val modulesMenu: ModulesMenu = ModulesMenu()
29 | ) {
30 | val currentHomepage by lazy {
31 | when {
32 | workingMode.isNonRoot -> Homepage.Repository
33 | else -> homepage
34 | }
35 | }
36 |
37 | @Composable
38 | fun isDarkMode() = when (darkMode) {
39 | DarkMode.AlwaysOff -> false
40 | DarkMode.AlwaysOn -> true
41 | DarkMode.FollowSystem -> isSystemInDarkTheme()
42 | }
43 |
44 | fun encodeTo(output: OutputStream) = output.write(
45 | ProtoBuf.encodeToByteArray(this)
46 | )
47 |
48 | companion object {
49 | fun decodeFrom(input: InputStream): UserPreferences =
50 | ProtoBuf.decodeFromByteArray(input.readBytes())
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ci_label.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
11 |
14 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.ui.theme
2 |
3 | import androidx.activity.ComponentActivity
4 | import androidx.activity.SystemBarStyle
5 | import androidx.activity.enableEdgeToEdge
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.SideEffect
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.toArgb
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | @Composable
15 | fun AppTheme(
16 | themeColor: Int,
17 | darkMode: Boolean = isSystemInDarkTheme(),
18 | content: @Composable () -> Unit
19 | ) {
20 | val color = Colors.getColor(id = themeColor)
21 | val colorScheme = when {
22 | darkMode -> color.darkColorScheme
23 | else -> color.lightColorScheme
24 | }
25 |
26 | SystemBarStyle(
27 | darkMode = darkMode
28 | )
29 |
30 | MaterialTheme(
31 | colorScheme = colorScheme,
32 | shapes = Shapes,
33 | typography = Typography,
34 | content = content
35 | )
36 | }
37 |
38 | @Composable
39 | private fun SystemBarStyle(
40 | darkMode: Boolean,
41 | statusBarScrim: Color = Color.Transparent,
42 | navigationBarScrim: Color = Color.Transparent
43 | ) {
44 | val context = LocalContext.current
45 | val activity = context as ComponentActivity
46 |
47 | SideEffect {
48 | activity.enableEdgeToEdge(
49 | statusBarStyle = SystemBarStyle.auto(
50 | statusBarScrim.toArgb(),
51 | statusBarScrim.toArgb(),
52 | ) { darkMode },
53 | navigationBarStyle = when {
54 | darkMode -> SystemBarStyle.dark(
55 | navigationBarScrim.toArgb()
56 | )
57 | else -> SystemBarStyle.light(
58 | navigationBarScrim.toArgb(),
59 | navigationBarScrim.toArgb(),
60 | )
61 | }
62 | )
63 | }
64 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Report a bug
3 | labels: [ "bug" ]
4 | title: "[BUG] "
5 | body:
6 | - type: checkboxes
7 | id: checklist
8 | attributes:
9 | label: Checklist
10 | description: Ensure that our bug report form is appropriate for you
11 | options:
12 | - label: No one has submitted a similar or identical bug report before
13 | required: true
14 | - label: I'm using the latest version of MRepo
15 | required: true
16 | - type: textarea
17 | id: bug
18 | attributes:
19 | label: Bug description
20 | description: Please describe the bug
21 | placeholder: |
22 | e.g. Crashed when installing module
23 | validations:
24 | required: true
25 | - type: textarea
26 | id: expected
27 | attributes:
28 | label: Expected behavior
29 | description: What did you expect to happen
30 | placeholder: |
31 | e.g. Install a module
32 | validations:
33 | required: true
34 | - type: textarea
35 | id: actual
36 | attributes:
37 | label: Actual behavior
38 | description: What happened instead
39 | placeholder: |
40 | e.g. Crashed
41 | validations:
42 | required: true
43 | - type: textarea
44 | id: steps
45 | attributes:
46 | label: Steps to reproduce
47 | description: How to reproduce the bug
48 | placeholder: |
49 | 1. Open the app
50 | 2. Crashed
51 |
52 | What an app
53 | - type: input
54 | id: ui
55 | attributes:
56 | label: UI / OS
57 | description: Your system UI or OS
58 | placeholder: MIUI / OneUI / etc.
59 | validations:
60 | required: true
61 | - type: input
62 | id: android
63 | attributes:
64 | label: Android Version
65 | description: Your Android Version
66 | placeholder: "14"
67 | validations:
68 | required: true
69 | - type: textarea
70 | id: additional
71 | attributes:
72 | label: Additional info
73 | description: Everything else you consider worthy that we didn't ask for
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/Compat.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import dev.sanmer.mrepo.datastore.model.WorkingMode
7 | import dev.sanmer.mrepo.stub.IModuleManager
8 | import dev.sanmer.su.IServiceManager
9 | import dev.sanmer.su.ServiceManagerCompat
10 | import dev.sanmer.su.ServiceManagerCompat.addService
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import timber.log.Timber
14 |
15 | object Compat {
16 | private var mServiceOrNull: IServiceManager? = null
17 | private val mService get() = checkNotNull(mServiceOrNull) {
18 | "IServiceManager haven't been received"
19 | }
20 |
21 | var isAlive by mutableStateOf(false)
22 | private set
23 |
24 | private val _isAliveFlow = MutableStateFlow(false)
25 | val isAliveFlow get() = _isAliveFlow.asStateFlow()
26 |
27 | val moduleManager: IModuleManager by lazy {
28 | IModuleManager.Stub.asInterface(
29 | mService.addService(
30 | ModuleManager::class.java
31 | )
32 | )
33 | }
34 |
35 | private fun state(): Boolean {
36 | isAlive = mServiceOrNull != null
37 | _isAliveFlow.value = isAlive
38 |
39 | return isAlive
40 | }
41 |
42 | suspend fun init(mode: WorkingMode) = when {
43 | isAlive -> true
44 | else -> try {
45 | mServiceOrNull = when (mode) {
46 | WorkingMode.Shizuku -> ServiceManagerCompat.fromShizuku()
47 | WorkingMode.Superuser -> ServiceManagerCompat.fromLibSu()
48 | else -> null
49 | }
50 |
51 | state()
52 | } catch (e: Throwable) {
53 | Timber.e(e)
54 |
55 | mServiceOrNull = null
56 | state()
57 | }
58 | }
59 |
60 | fun get(fallback: T, block: Compat.() -> T): T {
61 | return when {
62 | isAlive -> block(this)
63 | else -> fallback
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/dev/sanmer/mrepo/database/dao/LocalDao.kt:
--------------------------------------------------------------------------------
1 | package dev.sanmer.mrepo.database.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import androidx.room.Transaction
8 | import dev.sanmer.mrepo.database.entity.local.LocalModuleEntity
9 | import dev.sanmer.mrepo.model.local.LocalModule
10 | import kotlinx.coroutines.flow.Flow
11 |
12 | @Dao
13 | interface LocalDao {
14 | @Insert(onConflict = OnConflictStrategy.REPLACE)
15 | suspend fun insertLocal(value: LocalModuleEntity)
16 |
17 | @Insert(onConflict = OnConflictStrategy.REPLACE)
18 | suspend fun insertLocal(list: List)
19 |
20 | @Insert(onConflict = OnConflictStrategy.REPLACE)
21 | suspend fun insertUpdatable(value: LocalModuleEntity.Updatable)
22 |
23 | @Query("DELETE FROM local_updatable WHERE id NOT IN (:list)")
24 | suspend fun deleteUpdatableNotIn(list: List)
25 |
26 | @Query("DELETE FROM local WHERE id NOT IN (:list)")
27 | suspend fun deleteLocalNotIn(list: List)
28 |
29 | @Transaction
30 | suspend fun updateLocal(list: List) {
31 | val moduleIds = list.map { it.id }
32 |
33 | deleteUpdatableNotIn(moduleIds)
34 | deleteLocalNotIn(moduleIds)
35 | insertLocal(list.map { LocalModuleEntity(it) })
36 | }
37 |
38 | @Query("SELECT * FROM local")
39 | fun getLocalAllAsFlow(): Flow>
40 |
41 | @Query(
42 | "SELECT * FROM local " +
43 | "LEFT JOIN local_updatable ON local_updatable.id = local.id "
44 | )
45 | fun getLocalAndUpdatableAllAsFlow(): Flow