diff --git a/.github/actions/build-apk/action.yml b/.github/actions/build-apk/action.yml index cea5e54cb..f15a65591 100644 --- a/.github/actions/build-apk/action.yml +++ b/.github/actions/build-apk/action.yml @@ -93,19 +93,10 @@ runs: shell: bash run: | apk_path=$(find app/build/outputs/apk/${{ env.FLAVOR_LOWERCASE }}/${{ env.BUILD_TYPE_LOWERCASE }} -name "*.apk" -print -quit) - mapping_path=$(find app/build/outputs/mapping/${{ env.FLAVOR_LOWERCASE }}${{ env.BUILD_TYPE_CAPITALIZED }} -name "mapping.txt" -print -quit) name=$(basename "$apk_path" .apk) echo "name=$name" >> $GITHUB_OUTPUT echo "path=$apk_path" >> $GITHUB_OUTPUT - echo "mapping=$mapping_path" >> $GITHUB_OUTPUT - - - name: Upload mapping as artifact - if: ${{ steps.apk.outputs.mapping != '' }} - uses: actions/upload-artifact@v4 - with: - name: ${{ inputs.flavor }}-${{ inputs.buildType }}-mapping - path: ${{ steps.apk.outputs.mapping }} - name: Upload APK as artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index c90a05549..9886cfefd 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -80,6 +80,7 @@ jobs: id: create_release uses: actions/create-release@v1 with: + prerelease: ${{ github.event.inputs.track == 'beta' }} tag_name: v${{ env.VERSION }} release_name: v${{ env.VERSION }} body: | @@ -111,4 +112,4 @@ jobs: asset_name: ${{ steps.find_apks.outputs.SPOOFED_NAME }} asset_content_type: application/vnd.android.package-archive env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-ci.yml b/.github/workflows/release-ci.yml index c65853621..6dbf0ae36 100644 --- a/.github/workflows/release-ci.yml +++ b/.github/workflows/release-ci.yml @@ -30,8 +30,6 @@ on: options: - production - beta - - alpha - - qa jobs: # Build step for both official & spoofed flavors @@ -104,8 +102,8 @@ jobs: run: | [ -f gradle.properties ] && [ "$(tail -c1 gradle.properties)" != "" ] && echo >> gradle.properties { - echo "gpr.user=${{ inputs.username }}" - echo "gpr.key=${{ inputs.token }}" + echo "gpr.user=${{ secrets.ACTOR }}" + echo "gpr.key=${{ secrets.GH_TOKEN }}" } >> gradle.properties - name: Set up JDK diff --git a/.github/workflows/telegram.yml b/.github/workflows/telegram.yml index 13709fe20..19b57abaf 100644 --- a/.github/workflows/telegram.yml +++ b/.github/workflows/telegram.yml @@ -70,6 +70,7 @@ jobs: ${{ env.MESSAGE }} Branch: ${{ env.BRANCH_NAME }} + Track: ${{ github.event.inputs.track }} [Workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) files: | ${{ steps.find_apks.outputs.OFFICIAL_APK_PATH }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ae18e4ef..4ebd6c12f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -174,10 +174,14 @@ android { androidComponents { onVariants { variant -> + val distributionFlavor = variant.productFlavors + .firstOrNull { it.first == "distribution" } + ?.second + variant.outputs.filterIsInstance().forEach { output -> output.outputFileName.set( output.versionName.map { vName -> - "MMRL-${vName}-${variant.buildType ?: variant.name}.apk" + "MMRL-$vName-$distributionFlavor.apk" } ) } @@ -197,9 +201,6 @@ dependencies { implementation(projects.compat) implementation(projects.datastore) - implementation(libs.webuix.hwui) - implementation(libs.webuix.helper) - implementation(libs.kotlin.stdlib) implementation(libs.hiddenApiBypass) @@ -272,6 +273,8 @@ dependencies { ksp(libs.square.moshi.kotlin) implementation(libs.mmrlx.terminal) + implementation(libs.mmrlx.ui) + implementation(libs.mmrlx.utilities) implementation(libs.mmrlx.webui.core) implementation("dev.chrisbanes.haze:haze:1.6.10") diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/model/local/FeaturedManager.kt b/app/src/main/kotlin/com/dergoogler/mmrl/model/local/FeaturedManager.kt index 9f915262a..db4a3bc9c 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/model/local/FeaturedManager.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/model/local/FeaturedManager.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.dergoogler.mmrl.R import com.dergoogler.mmrl.datastore.model.WorkingMode -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.RadioDialogItem +import dev.mmrlx.compose.ui.list.component.RadioDialogOption data class FeaturedManager( private val nameAs: Any, @@ -15,7 +15,7 @@ data class FeaturedManager( ) { @Composable fun toRadioDialogItem() = - RadioDialogItem( + RadioDialogOption( title = name, value = workingMode, ) diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/pathHandler/AssetsPathHandler.kt b/app/src/main/kotlin/com/dergoogler/mmrl/pathHandler/AssetsPathHandler.kt deleted file mode 100644 index b8255883f..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/pathHandler/AssetsPathHandler.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.dergoogler.mmrl.pathHandler - -import android.util.Log -import android.webkit.WebResourceResponse -import com.dergoogler.mmrl.hybridwebui.HybridWebUI -import dev.mmrlx.webui.PathHandler -import dev.mmrlx.webui.WebUI -import dev.mmrlx.webui.WebUIResourceRequest -import java.io.IOException - -class AssetsPathHandler( - webui: WebUI, -) : PathHandler(webui) { - private val assetHelper get() = kontext.assets - - override fun handle(request: WebUIResourceRequest): WebResourceResponse { - val path = request.path - - try { - val inputStream = assetHelper.open(path.removePrefix("/")) - val mimeType = HybridWebUI.MimeType.getMimeFromFileName(path) - return WebResourceResponse(mimeType, null, inputStream) - } catch (e: IOException) { - Log.e("assetsPathHandler", "Error opening asset path: $path", e) - return notFoundResponse - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/pathHandler/InternalPathHandler.kt b/app/src/main/kotlin/com/dergoogler/mmrl/pathHandler/InternalPathHandler.kt deleted file mode 100644 index c1e90cad5..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/pathHandler/InternalPathHandler.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.dergoogler.mmrl.pathHandler - - -import android.util.Log -import android.webkit.WebResourceResponse -import androidx.compose.material3.ColorScheme -import com.dergoogler.mmrl.model.WebColors -import dev.mmrlx.webui.PathHandler -import dev.mmrlx.webui.WebUI -import dev.mmrlx.webui.WebUIResourceRequest -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URL - -class InternalPathHandler( - webui: WebUI, - private val readmeUrl: String, - private val colorScheme: ColorScheme, -) : PathHandler(webui) { - override val id = "/internal/" - val webColors get() = WebColors(colorScheme) - val assetsPathHandler = AssetsPathHandler(this) - - override fun handle(request: WebUIResourceRequest): WebResourceResponse { - val path = request.path - - try { - if (path.matches(Regex("^assets(/.*)?$"))) { - return assetsPathHandler.handle( - WebUIResourceRequest( - method = request.method, - isForMainFrame = request.isForMainFrame, - url = request.url, - path = path.removePrefix("assets/"), - requestHeaders = request.requestHeaders, - isRedirect = request.isRedirect, - hasGesture = request.hasGesture() - ) - ) - } - - - - if (path.matches(Regex("readme\\.md"))) { - val connection = URL(readmeUrl).openConnection() as HttpURLConnection - connection.connect() - return htmlResponse(connection.getInputStream()) - } - - if (path.matches(Regex("insets\\.css"))) { - return insets.css.asStyleResponse() - } - - if (path.matches(Regex("colors\\.css"))) { - return webColors.allCssColors.asStyleResponse() - } - - return notFoundResponse - } catch (e: IOException) { - Log.e("InternalPathHandler", "Error opening mmrl asset path: $path", e) - return notFoundResponse - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MMRLComponentActivity.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MMRLComponentActivity.kt index 82d20f0f0..ad77ea39e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MMRLComponentActivity.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/MMRLComponentActivity.kt @@ -34,18 +34,17 @@ import com.dergoogler.mmrl.ui.providable.LocalLifecycleScope import com.dergoogler.mmrl.ui.providable.LocalMainNavController import com.dergoogler.mmrl.ui.providable.LocalNavController import com.dergoogler.mmrl.ui.providable.LocalSettings -import com.dergoogler.mmrl.ui.providable.LocalSuperUserViewModel import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.ui.providable.LocalWindowSizeClass import com.dergoogler.mmrl.ui.theme.Colors import com.dergoogler.mmrl.ui.theme.MMRLAppTheme import com.dergoogler.mmrl.viewmodel.SettingsViewModel -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import dagger.hilt.android.AndroidEntryPoint import dev.dergoogler.mmrl.compat.BuildCompat import dev.dergoogler.mmrl.compat.core.BrickException import dev.dergoogler.mmrl.compat.core.MMRLUriHandlerImpl +import dev.mmrlx.compose.ui.theme.MMRLXTheme import timber.log.Timber import javax.inject.Inject import kotlin.system.exitProcess @@ -216,31 +215,36 @@ fun BaseContent( val currentTheme = Colors.getColor(preferences.themeColor, preferences.isDarkMode()) val toolbarColor = currentTheme.surface - MMRLAppTheme( - darkMode = preferences.isDarkMode(), - navController = navController, - themeColor = preferences.themeColor, - providerValues = - arrayOf( - LocalWindowSizeClass provides windowSizeClass, - LocalDestinationsNavigator provides navigator, - LocalActivity provides activity, - LocalSuperUserViewModel provides hiltViewModel(activity), - LocalSettings provides hiltViewModel(activity), - LocalUserPreferences provides preferences, - dev.dergoogler.mmrl.compat.core.LocalUriHandler provides - MMRLUriHandlerImpl( - context, - toolbarColor, - ), - LocalLifecycleScope provides activity.lifecycleScope, - LocalLifecycle provides activity.lifecycle, - LocalUriHandler provides MMRLUriHandlerImpl(context, toolbarColor), - LocalNavController provides navController, - LocalMainNavController provides mainNavController, - ), - content = content, - ) + val darkMode = preferences.isDarkMode() + + MMRLXTheme( + darkTheme = darkMode, + ) { + MMRLAppTheme( + darkMode = darkMode, + navController = navController, + themeColor = preferences.themeColor, + providerValues = + arrayOf( + LocalWindowSizeClass provides windowSizeClass, + LocalDestinationsNavigator provides navigator, + LocalActivity provides activity, + LocalSettings provides hiltViewModel(activity), + LocalUserPreferences provides preferences, + dev.dergoogler.mmrl.compat.core.LocalUriHandler provides + MMRLUriHandlerImpl( + context, + toolbarColor, + ), + LocalLifecycleScope provides activity.lifecycleScope, + LocalLifecycle provides activity.lifecycle, + LocalUriHandler provides MMRLUriHandlerImpl(context, toolbarColor), + LocalNavController provides navController, + LocalMainNavController provides mainNavController, + ), + content = content, + ) + } } fun MMRLComponentActivity.setBaseContent( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/action/ActionScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/action/ActionScreen.kt index 1aea464bc..4214e0de1 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/action/ActionScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/action/ActionScreen.kt @@ -46,6 +46,8 @@ import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.viewmodel.ActionViewModel import dev.chrisbanes.haze.hazeSource import dev.mmrlx.compose.terminal.Terminal +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior import dev.mmrlx.terminal.TerminalEmulator.Companion.TERMINAL_CURSOR_STYLE_NONE import kotlinx.coroutines.launch @@ -54,7 +56,7 @@ fun ActionScreen(viewModel: ActionViewModel) = LocalScreenProvider { val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = ToolbarDefaults.pinnedScrollBehavior() val userPreferences = LocalUserPreferences.current val context = LocalContext.current @@ -174,7 +176,7 @@ private fun TopBar( exportLog: () -> Unit, event: Event, onBack: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior, + scrollBehavior: ToolbarScrollBehavior, enable: Boolean, ) = BlurNavigateUpToolbar( title = stringResource(id = R.string.action_activity), diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/install/InstallScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/install/InstallScreen.kt index 852553a1a..c10393df8 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/install/InstallScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/activity/terminal/install/InstallScreen.kt @@ -54,6 +54,8 @@ import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.viewmodel.InstallViewModel import dev.chrisbanes.haze.hazeSource import dev.mmrlx.compose.terminal.Terminal +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior import dev.mmrlx.terminal.TerminalEmulator.Companion.TERMINAL_CURSOR_STYLE_NONE import kotlinx.coroutines.launch @@ -62,7 +64,7 @@ fun InstallScreen(viewModel: InstallViewModel) = LocalScreenProvider { val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = ToolbarDefaults.pinnedScrollBehavior() val userPreferences = LocalUserPreferences.current val context = LocalContext.current @@ -226,7 +228,7 @@ private fun TopBar( exportLog: () -> Unit, event: Event, onBack: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior, + scrollBehavior: ToolbarScrollBehavior, enable: Boolean, ) = BlurNavigateUpToolbar( title = stringResource(id = R.string.install_screen_title), diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/AntiFeaturesItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/AntiFeaturesItem.kt index 476e6c6c6..db46d0ce7 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/AntiFeaturesItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/AntiFeaturesItem.kt @@ -3,10 +3,10 @@ package com.dergoogler.mmrl.ui.component import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.list.component.Item +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Title @Composable fun ListScope.AntiFeaturesItem( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/BottomBar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/BottomBar.kt new file mode 100644 index 000000000..e7f784a2b --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/BottomBar.kt @@ -0,0 +1,158 @@ +package com.dergoogler.mmrl.ui.component + +import androidx.compose.foundation.layout.imePadding +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.dergoogler.mmrl.ui.navigation.MainDestination +import com.dergoogler.mmrl.ui.navigation.isAccessible +import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator +import com.dergoogler.mmrl.ui.providable.LocalNavController +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.ramcosta.composedestinations.generated.NavGraphs +import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.navigationbar.NavigationBar +import dev.mmrlx.compose.ui.navigationbar.NavigationBarItem +import dev.mmrlx.compose.ui.theme.MMRLXTheme + +@Composable +fun BottomBar(updates: Int = 0) { + val prefs = LocalUserPreferences.current + val navigator = LocalDestinationsNavigator.current + val navController = LocalNavController.current + + NavigationBar( + modifier = + Modifier + .imePadding() + ) { + for (screen in MainDestination.entries) { + if (!screen.isAccessible) continue + + val isSelected by navController.isRouteOnBackStackAsState(screen.direction) + + NavigationBarItem( + icon = { + BaseNavIcon(screen, isSelected, updates) + }, + label = { + if (!prefs.hideBottomBarLabels) { + Text( + text = stringResource(id = screen.label), + style = MMRLXTheme.typography.labelLarge, + ) + } + }, + selected = isSelected, + onClick = { + if (isSelected) { + navigator.popBackStack(screen.direction, false) + } + navigator.navigate(screen.direction) { + popUpTo(NavGraphs.root) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + ) + } + } +} +/* +@Composable +private fun RailNav(updates: Int) { + val prefs = LocalUserPreferences.current + val navigator = LocalDestinationsNavigator.current + val navController = LocalNavController.current + + NavigationRail( + header = { + TopAppBarEventIcon() + }, + ) { + MainDestination.entries.forEach { screen -> + val isSelected by navController.isRouteOnBackStackAsState(screen.direction) + + if (!screen.isAccessible) return@forEach + + NavigationRailItem( + icon = { + BaseNavIcon(screen, isSelected, updates) + }, + label = { + Text( + text = stringResource(id = screen.label), + style = MaterialTheme.typography.labelLarge, + ) + }, + alwaysShowLabel = !prefs.hideBottomBarLabels, + selected = isSelected, + onClick = { + if (isSelected) { + navigator.popBackStack(screen.direction, false) + } + navigator.navigate(screen.direction) { + popUpTo(NavGraphs.root) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + ) + } + } +}*/ + +@Composable +private fun BaseNavIcon( + screen: MainDestination, + selected: Boolean, + updates: Int, +) { + if (screen == MainDestination.Modules && updates > 0) { + BadgedBox( + badge = { + Badge { + Text(text = updates.toString()) + } + }, + ) { + Icon( + painter = + painterResource( + id = + if (selected) { + screen.iconFilled + } else { + screen.icon + }, + ), + contentDescription = null, + ) + } + + return + } + + Icon( + painter = + painterResource( + id = + if (selected) { + screen.iconFilled + } else { + screen.icon + }, + ), + contentDescription = null, + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/PermissionItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/PermissionItem.kt index fa0502746..c65d61aef 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/PermissionItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/PermissionItem.kt @@ -6,17 +6,17 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.runtime.Composable import com.dergoogler.mmrl.R import com.dergoogler.mmrl.ext.nullable -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.list.component.Item +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Supporting +import dev.mmrlx.compose.ui.list.component.item.Title @Composable fun ListScope.PermissionItem( permissions: List, contentPadding: PaddingValues, -) = permissions.forEach { +) = permissions.forEach { it -> val permission = getPermissionDetails(it) permission.nullable { p -> Item( @@ -25,7 +25,7 @@ fun ListScope.PermissionItem( Title(p.title) Description(p.desc) p.rootSolutions.nullable { - Labels { + Supporting { it() } } @@ -80,7 +80,7 @@ private fun getPermissionDetails(id: String): Permission? { { KernelSuLabel() APatchLabel() - MMRLLabel() + WebUIXLabel() }, ), "kernelsu.permission.POST_MOUNT" to @@ -111,7 +111,7 @@ private fun getPermissionDetails(id: String): Permission? { Permission( R.string.view_module_features_mmrl_webui_config, R.string.view_module_features_mmrl_webui_config_sub, - { MMRLLabel() }, + { WebUIXLabel() }, ), "mmrl.permission.APKS" to Permission( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SELinuxStatus.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SELinuxStatus.kt index b7f89725d..e1cc68d67 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SELinuxStatus.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/SELinuxStatus.kt @@ -3,15 +3,14 @@ package com.dergoogler.mmrl.ui.component import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.res.painterResource import com.dergoogler.mmrl.R import com.dergoogler.mmrl.platform.PlatformManager -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.list.component.Item +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Icon +import dev.mmrlx.compose.ui.list.component.item.Title import com.dergoogler.mmrl.ui.remember.remember @Composable diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Scaffold.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Scaffold.kt index 42f585c19..78b8f0884 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Scaffold.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/Scaffold.kt @@ -1,115 +1,96 @@ package com.dergoogler.mmrl.ui.component import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import com.dergoogler.mmrl.ext.none -import com.dergoogler.mmrl.ext.systemBarsPaddingEnd -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.scaffold.ResponsiveScaffold -import com.dergoogler.mmrl.ui.component.toolbar.BlurNavigateUpToolbar -import com.dergoogler.mmrl.ui.component.toolbar.BlurToolbar -import com.dergoogler.mmrl.ui.component.toolbar.ToolbarTitle -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings import com.dergoogler.mmrl.ui.providable.LocalNavController -import dev.chrisbanes.haze.hazeSource +import dev.mmrlx.compose.ui.list.List +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.scaffold.Scaffold +import dev.mmrlx.compose.ui.toolbar.NavigateUpToolbar +import dev.mmrlx.compose.ui.toolbar.Toolbar +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarTitle @Composable fun SettingsScaffold( - modifier: ScaffoldModifier = ScaffoldDefaults.settingsScaffoldScrollModifier, @StringRes title: Int, - allowNavigateBack: Boolean = true, + isMainPage: Boolean = false, + canScroll: Boolean = true, actions: @Composable (RowScope.() -> Unit) = {}, floatingActionButton: @Composable () -> Unit = {}, - absolute: @Composable (BoxScope.() -> Unit) = {}, relative: @Composable (ListScope.() -> Unit), ) = SettingsScaffold( title = stringResource(id = title), - modifier = modifier, actions = actions, - allowNavigateBack = allowNavigateBack, + isMainPage = isMainPage, + canScroll = canScroll, floatingActionButton = floatingActionButton, - absolute = absolute, relative = relative, ) @Composable fun SettingsScaffold( - modifier: ScaffoldModifier = ScaffoldDefaults.settingsScaffoldScrollModifier, title: String, - allowNavigateBack: Boolean = true, + isMainPage: Boolean = false, + canScroll: Boolean = true, actions: @Composable (RowScope.() -> Unit) = {}, floatingActionButton: @Composable () -> Unit = {}, - absolute: @Composable (BoxScope.() -> Unit) = {}, relative: @Composable (ListScope.() -> Unit), -) = LocalScreenProvider { +) { val navController = LocalNavController.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = ToolbarDefaults.fadePinnedScrollBehavior() + + val scrollModifier = if (canScroll) { + Modifier.verticalScroll(rememberScrollState()) + } else Modifier - ResponsiveScaffold( + Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - if (allowNavigateBack) { - BlurNavigateUpToolbar( + toolbar = { + if (!isMainPage) { + NavigateUpToolbar( title = title, navController = navController, actions = actions, - fade = true, - fadeDistance = 50f, scrollBehavior = scrollBehavior, ) } else { - BlurToolbar( + Toolbar( title = { ToolbarTitle(title = title) }, - fade = true, - fadeDistance = 50f, actions = actions, scrollBehavior = scrollBehavior, ) } }, + bottomBar = { + if (isMainPage) { + BottomBar() + } + }, floatingActionButton = floatingActionButton, contentWindowInsets = WindowInsets.none, - ) { innerPadding -> - Box( - modifier = - Modifier - .hazeSource(state = LocalHazeState.current) - .then(modifier.box), - ) { - List( - modifier = - modifier.column - .systemBarsPaddingEnd() - .padding(top = innerPadding.calculateTopPadding()), - content = { - relative() - - val paddingValues = LocalMainScreenInnerPaddings.current - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) - }, - ) - - absolute() - } + ) { + List( + modifier = Modifier + .then(scrollModifier) + .scaffoldHazeSource() + .scaffoldPadding(), + content = { + relative() + }, + ) } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/TopAppBarEventIcon.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/TopAppBarEventIcon.kt index 31d37e462..349771beb 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/TopAppBarEventIcon.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/TopAppBarEventIcon.kt @@ -1,11 +1,22 @@ package com.dergoogler.mmrl.ui.component +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R import com.dergoogler.mmrl.datastore.providable.LocalUserPreferences -import com.dergoogler.mmrl.ui.component.toolbar.ToolbarEventIcon import com.dergoogler.mmrl.ui.model.toolbar.ToolbarEvent +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.theme.LocalContentColor +import dev.mmrlx.compose.ui.toolbar.ToolbarTitle @Composable fun TopAppBarEventIcon() { @@ -52,3 +63,63 @@ fun TopAppBarEventIcon() { events = events, ) } + +@Composable +fun ToolbarIcon( + modifier: Modifier = Modifier, + @DrawableRes icon: Int = com.dergoogler.mmrl.ui.R.drawable.mmrl_logo, + subtitle: String? = null, + tint: Color = MaterialTheme.colorScheme.surfaceTint, +) = ToolbarTitle( + modifier = modifier, + titleContent = { + Icon( + modifier = Modifier.size(30.dp), + painter = painterResource(id = icon), + contentDescription = null, + tint = tint, + ) + }, + subtitleContent = { + subtitle?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = LocalContentColor.current.copy(alpha = 0.75f), + ) + } + } +) + +@Composable +fun ToolbarEventIcon( + events: List, + modifier: Modifier = Modifier, + enabled: Boolean = true, + @DrawableRes nonEventIcon: Int = com.dergoogler.mmrl.ui.R.drawable.mmrl_logo, +) { + if (!enabled) { + return ToolbarIcon( + modifier = modifier, + icon = nonEventIcon, + ) + } + + val event = remember(events) { events.firstOrNull { it.isActive } } + + if (event != null) { + ToolbarIcon( + modifier = modifier, + icon = event.icon, + subtitle = event.getTitle(), + ) + } else { + ToolbarIcon( + modifier = modifier, + icon = nonEventIcon, + ) + } +} + diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurNavigateUpToolbar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurNavigateUpToolbar.kt index 820d1b258..055f6fee9 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurNavigateUpToolbar.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurNavigateUpToolbar.kt @@ -8,7 +8,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -17,6 +16,8 @@ import androidx.navigation.NavController import com.dergoogler.mmrl.ext.takeTrue import com.dergoogler.mmrl.ui.R import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -28,7 +29,7 @@ fun BlurNavigateUpToolbar( enable: Boolean = true, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - scrollBehavior: TopAppBarScrollBehavior? = null, + scrollBehavior: ToolbarScrollBehavior? = null, fade: Boolean = false, fadeDistance: Float = 200f, fadeBackgroundIfNoBlur: Boolean = false, @@ -56,7 +57,7 @@ fun BlurNavigateUpToolbar( enable: Boolean = true, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - scrollBehavior: TopAppBarScrollBehavior? = null, + scrollBehavior: ToolbarScrollBehavior? = null, fade: Boolean = false, fadeDistance: Float = 200f, fadeBackgroundIfNoBlur: Boolean = false, @@ -84,7 +85,7 @@ fun BlurNavigateUpToolbar( enable: Boolean = true, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - scrollBehavior: TopAppBarScrollBehavior? = null, + scrollBehavior: ToolbarScrollBehavior? = null, fade: Boolean = false, fadeDistance: Float = 200f, fadeBackgroundIfNoBlur: Boolean = false, @@ -112,7 +113,7 @@ fun BlurNavigateUpToolbar( enable: Boolean = true, actions: @Composable RowScope.() -> Unit = {}, windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - scrollBehavior: TopAppBarScrollBehavior? = null, + scrollBehavior: ToolbarScrollBehavior? = null, fade: Boolean = false, fadeDistance: Float = 200f, fadeBackgroundIfNoBlur: Boolean = false, @@ -139,8 +140,8 @@ fun BlurNavigateUpToolbar( modifier: Modifier = Modifier, enable: Boolean = true, actions: @Composable RowScope.() -> Unit = {}, - windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - scrollBehavior: TopAppBarScrollBehavior? = null, + windowInsets: WindowInsets = ToolbarDefaults.windowInsets, + scrollBehavior: ToolbarScrollBehavior? = null, fade: Boolean = false, fadeDistance: Float = 200f, fadeBackgroundIfNoBlur: Boolean = false, diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurSearchToolbar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurSearchToolbar.kt index 454e81dd2..8c8db2382 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurSearchToolbar.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurSearchToolbar.kt @@ -5,15 +5,8 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -28,8 +21,14 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.ui.R +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton +import dev.mmrlx.compose.ui.theme.MMRLXTheme +import dev.mmrlx.compose.ui.toolbar.Toolbar +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior -@OptIn(ExperimentalMaterial3Api::class) @Composable fun BlurSearchToolbar( modifier: Modifier = Modifier, @@ -41,19 +40,13 @@ fun BlurSearchToolbar( title: @Composable () -> Unit, navigationIcon: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, - windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - scrollBehavior: TopAppBarScrollBehavior? = null, - fadeBackgroundIfNoBlur: Boolean = false, - fadeDistance: Float = 200f, - fade: Boolean = false, -) = BlurToolbar( + windowInsets: WindowInsets = ToolbarDefaults.windowInsets, + scrollBehavior: ToolbarScrollBehavior? = null, +) = Toolbar( modifier = modifier, actions = actions, windowInsets = windowInsets, scrollBehavior = scrollBehavior, - fade = fade, - fadeBackgroundIfNoBlur = fadeBackgroundIfNoBlur, - fadeDistance = fadeDistance, navigationIcon = if (onClose != null && isSearch) { { @@ -111,7 +104,7 @@ fun BlurSearchToolbar( Text(text = stringResource(id = R.string.search_placeholder)) }, singleLine = true, - textStyle = MaterialTheme.typography.bodyLarge, + textStyle = MMRLXTheme.typography.bodyLarge, ) } } else { diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurToolbar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurToolbar.kt index aa9238b6f..e446e90f6 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurToolbar.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/component/toolbar/BlurToolbar.kt @@ -5,131 +5,169 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp -import com.dergoogler.mmrl.ui.providable.LocalHazeState import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.utils.BlurUtil -import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials +import dev.mmrlx.compose.ui.theme.MMRLXTheme +import dev.mmrlx.compose.ui.theme.Oklch +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior + +private const val VISIBILITY_THRESHOLD = 0.01f + +/** + * Read-only view of the toolbar's current visual state. + * All properties are snapshot-backed, so reading them in composition + * (or in a draw/layout lambda) will trigger the right invalidation. + */ +@Stable +class BlurToolbarState internal constructor( + private val animatedAlpha: State, + private val targetAlphaState: State, + private val blurEnabled: State, + private val scrollBehavior: ToolbarScrollBehavior?, +) { + /** Current (animated) alpha, 0f..1f. */ + val alpha: Float get() = animatedAlpha.value + + /** Un-animated alpha derived from scroll position. */ + val targetAlpha: Float get() = targetAlphaState.value + + /** Blur is enabled in prefs AND supported on this device. */ + val isBlurEnabled: Boolean get() = blurEnabled.value + + /** True if the toolbar is visible enough to draw anything. */ + val isVisible: Boolean get() = alpha > VISIBILITY_THRESHOLD + + /** True if the haze blur effect is actually being applied. */ + val isBlurActive: Boolean get() = isBlurEnabled && isVisible + + val isFullyVisible: Boolean get() = alpha >= 1f - VISIBILITY_THRESHOLD + + /** True while the alpha animation is still catching up with its target. */ + val isAnimating: Boolean get() = alpha != targetAlpha + + /** Raw scroll content offset (<= 0 when scrolled), or 0f without a scroll behavior. */ + val contentOffset: Float get() = scrollBehavior?.state?.contentOffset ?: 0f + + /** 0f = expanded, 1f = fully collapsed. */ + val collapsedFraction: Float get() = scrollBehavior?.state?.collapsedFraction ?: 0f + + /** 0f..1f, how much content overlaps the toolbar. */ + val overlappedFraction: Float get() = scrollBehavior?.state?.overlappedFraction ?: 0f +} -@OptIn(ExperimentalHazeMaterialsApi::class) @Composable -fun BlurToolbar( - title: @Composable (Float) -> Unit, - modifier: Modifier = Modifier, - navigationIcon: @Composable () -> Unit = {}, - actions: @Composable RowScope.() -> Unit = {}, - expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight, - windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, - fadeBackgroundIfNoBlur: Boolean = false, - fadeDistance: Float = 200f, +fun rememberBlurToolbarState( + scrollBehavior: ToolbarScrollBehavior? = null, fade: Boolean = false, - scrollBehavior: TopAppBarScrollBehavior? = null, -) { + fadeDistance: Float = 200f, +): BlurToolbarState { val prefs = LocalUserPreferences.current - val state = scrollBehavior?.state + val scrollState = scrollBehavior?.state - val isBlurEnabled = - remember(prefs) { - prefs.enableBlur && BlurUtil.isBlurSupported() - } + val blurEnabled = remember(prefs) { + derivedStateOf { prefs.enableBlur && BlurUtil.isBlurSupported() } + } - val targetAlpha by remember { + val targetAlpha = remember(scrollState, fade, fadeDistance) { derivedStateOf { - if (!fade || state == null) { + if (!fade || scrollState == null || fadeDistance <= 0f) { 1f } else { - ((-state.contentOffset) / fadeDistance).coerceIn(0f, 1f) + (-scrollState.contentOffset / fadeDistance).coerceIn(0f, 1f) } } } - val animatedAlpha by animateFloatAsState( - targetValue = targetAlpha, - animationSpec = - tween( - durationMillis = 10, - easing = FastOutSlowInEasing, - ), + val animatedAlpha = animateFloatAsState( + targetValue = targetAlpha.value, + animationSpec = tween(durationMillis = 150, easing = FastOutSlowInEasing), label = "toolbar_alpha", ) - val borderColor = MaterialTheme.colorScheme.outline - val backgroundColor = MaterialTheme.colorScheme.background + return remember(scrollBehavior, blurEnabled, targetAlpha, animatedAlpha) { + BlurToolbarState( + animatedAlpha = animatedAlpha, + targetAlphaState = targetAlpha, + blurEnabled = blurEnabled, + scrollBehavior = scrollBehavior, + ) + } +} - val topAppBarColors = TopAppBarDefaults.topAppBarColors() +@OptIn(ExperimentalHazeMaterialsApi::class) +@Composable +fun BlurToolbar( + title: @Composable (Float) -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + fadeBackgroundIfNoBlur: Boolean = false, + fadeDistance: Float = 200f, + fade: Boolean = false, + scrollBehavior: ToolbarScrollBehavior? = null, + state: BlurToolbarState = rememberBlurToolbarState( + scrollBehavior = scrollBehavior, + fade = fade, + fadeDistance = fadeDistance, + ), +) { + val borderColor = MMRLXTheme.border.okLchColor + val backgroundColor = MMRLXTheme.oklchColors.background + val topAppBarColors = ToolbarDefaults.colors() - val colors = - if (isBlurEnabled) { - topAppBarColors.copy( - scrolledContainerColor = Color.Transparent, - containerColor = Color.Transparent, - ) + val colors = if (state.isBlurEnabled) { + topAppBarColors.copy( + scrolledContainerColor = Oklch(0.0,0.0, 0.0,0.0), + containerColor = Oklch(0.0,0.0, 0.0,0.0), + ) + } else { + val container = if (fadeBackgroundIfNoBlur) { + backgroundColor.copy(opacity = state.alpha.toDouble()) } else { - val backgroundColor = - MaterialTheme.colorScheme.background - .let { - if (fadeBackgroundIfNoBlur) { - it.copy(alpha = animatedAlpha) - } else { - it - } - } - - topAppBarColors.copy( - scrolledContainerColor = backgroundColor, - containerColor = backgroundColor, - ) + backgroundColor } + topAppBarColors.copy( + scrolledContainerColor = container, + containerColor = container, + ) + } - val blur = - if (isBlurEnabled && animatedAlpha > 0.01f) { - Modifier.hazeEffect( - state = LocalHazeState.current, - style = HazeMaterials.ultraThin(), - ) { - this@hazeEffect.backgroundColor = backgroundColor - this@hazeEffect.alpha = animatedAlpha + dev.mmrlx.compose.ui.toolbar.Toolbar( + title = { title(state.alpha) }, + modifier = Modifier + .drawBehind { + // Read inside the draw scope so alpha changes only redraw + val alpha = state.alpha + if (alpha > VISIBILITY_THRESHOLD) { + val borderSize = Dp.Hairline + val y = size.height - borderSize.value + drawLine( + color = borderColor.copy(opacity = alpha.toDouble()).toColor(), + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = borderSize.value, + ) + } } - } else { - Modifier - } - - TopAppBar( - title = { title(animatedAlpha) }, - modifier = - Modifier - .drawBehind { - if (animatedAlpha > 0.01f) { - val borderSize = Dp.Hairline - val y = size.height - borderSize.value - drawLine( - color = borderColor.copy(alpha = animatedAlpha), - start = Offset(0f, y), - end = Offset(size.width, y), - strokeWidth = borderSize.value, - ) - } - }.then(blur) - .then(modifier), + .then(modifier), navigationIcon = navigationIcon, actions = actions, colors = colors, scrollBehavior = scrollBehavior, windowInsets = windowInsets, - expandedHeight = expandedHeight, ) -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/MainDestination.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/MainDestination.kt index 0962e3c0b..f52c009ff 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/MainDestination.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/navigation/MainDestination.kt @@ -4,14 +4,11 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.runtime.Composable import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.platform.ksu.KsuNative import com.dergoogler.mmrl.ui.remember.rememberIsRoot -import com.dergoogler.mmrl.utils.isManager import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination import com.ramcosta.composedestinations.generated.destinations.ModulesScreenDestination import com.ramcosta.composedestinations.generated.destinations.RepositoriesScreenDestination import com.ramcosta.composedestinations.generated.destinations.SettingsScreenDestination -import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination import com.ramcosta.composedestinations.spec.DirectionDestinationSpec // @NavHostGraph @@ -34,15 +31,6 @@ enum class MainDestination( requiresKernel = false, ), - SuperUser( - direction = SuperUserScreenDestination, - label = R.string.page_superuser, - icon = R.drawable.user, - iconFilled = R.drawable.user_filled, - requiresRoot = true, - requiresKernel = true, - ), - Repository( direction = RepositoriesScreenDestination, label = R.string.page_repositorys, @@ -75,7 +63,5 @@ enum class MainDestination( val MainDestination.isAccessible: Boolean get() { val isRoot = rememberIsRoot() - if (this.requiresRoot && !isRoot) return false - if (this.requiresKernel && !(KsuNative.isManager && isRoot)) return false - return true + return !(this.requiresRoot && !isRoot) } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalScrollBehavior.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalScrollBehavior.kt index 426ddc75a..528e62131 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalScrollBehavior.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalScrollBehavior.kt @@ -1,9 +1,9 @@ package com.dergoogler.mmrl.ui.providable -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.staticCompositionLocalOf +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior val LocalScrollBehavior = - staticCompositionLocalOf { + staticCompositionLocalOf { error("CompositionLocal LocalScrollBehavior not present") } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalSuperUserViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalSuperUserViewModel.kt deleted file mode 100644 index c58ef3c51..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/providable/LocalSuperUserViewModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.dergoogler.mmrl.ui.providable - -import androidx.compose.runtime.staticCompositionLocalOf -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel - -val LocalSuperUserViewModel = - staticCompositionLocalOf { - error("CompositionLocal LocalSuperUserViewModel not present") - } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/AppProfileScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/AppProfileScreen.kt deleted file mode 100644 index 96a9c88dc..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/AppProfileScreen.kt +++ /dev/null @@ -1,232 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.appProfile - -import androidx.annotation.StringRes -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ext.nullable -import com.dergoogler.mmrl.platform.ksu.KsuNative -import com.dergoogler.mmrl.ui.component.LocalScreen -import com.dergoogler.mmrl.ui.component.LocalScreenProvider -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.RadioDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Start -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title -import com.dergoogler.mmrl.ui.component.toolbar.BlurNavigateUpToolbar -import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings -import com.dergoogler.mmrl.ui.providable.LocalSnackbarHost -import com.dergoogler.mmrl.ui.screens.appProfile.items.RootProfileConfig -import com.dergoogler.mmrl.ui.screens.appProfile.remember.LocalProfileChange -import com.dergoogler.mmrl.ui.screens.appProfile.remember.rememberProfileChange -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel.AppInfo.Companion.loadIcon -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import dev.chrisbanes.haze.hazeSource - -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun AppProfileScreen(appInfo: SuperUserViewModel.AppInfo) = - LocalScreenProvider { - val context = LocalContext.current - val snackBarHost = LocalSnackbarHost.current - val bottomBarPaddingValues = LocalMainScreenInnerPaddings.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - - val (profile, onProfileChange) = rememberProfileChange(appInfo) - - val icon = - remember { - appInfo.loadIcon(context) - } - - CompositionLocalProvider( - LocalProfileChange provides (profile to onProfileChange), - ) { - Scaffold( - modifier = - Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - BlurNavigateUpToolbar( - fade = true, - fadeDistance = 50f, - navigator = LocalDestinationsNavigator.current, - title = stringResource(R.string.profile), - scrollBehavior = scrollBehavior, - ) - }, - snackbarHost = { - LocalScreen.SnackbarHost() - }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - ) { paddingValues -> - val isRootGranted = profile.allowSu - - var mode by rememberSaveable(profile, isRootGranted) { - val initialMode = - if (isRootGranted) { - if (profile.rootUseDefault) Mode.Default else Mode.Custom - } else { - if (profile.nonRootUseDefault) Mode.Default else Mode.Custom - } - mutableStateOf(initialMode) - } - - List( - modifier = - Modifier - .hazeSource(LocalHazeState.current) - .verticalScroll(rememberScrollState()), - ) { - Spacer(Modifier.height(paddingValues.calculateTopPadding())) - - Item { - Title(appInfo.label) - Description(appInfo.packageName) - Start { - AsyncImage( - model = icon, - contentDescription = appInfo.label, - modifier = Modifier.size(48.dp), - ) - } - } - - SwitchItem( - checked = isRootGranted, - onChange = { - onProfileChange(profile.copy(allowSu = it)) - }, - ) { - Icon( - painter = painterResource(R.drawable.square_root_2), - ) - Title(R.string.allow_root_access) - Description(R.string.allow_root_access_desc) - } - - RadioDialogItem( - selection = mode, - options = mode.dialogOptions, - onConfirm = { - if (isRootGranted) { - if (it.value == Mode.Default || it.value == Mode.Custom) { - onProfileChange(profile.copy(rootUseDefault = it.value == Mode.Default)) - } - - mode = it.value - - return@RadioDialogItem - } - - onProfileChange(profile.copy(nonRootUseDefault = (it.value == Mode.Default))) - }, - ) { - Icon(painter = painterResource(R.drawable.disabled)) - Title(R.string.profile) - it.title.nullable { t -> - Description(t) - } - } - - Crossfade( - targetState = (isRootGranted to mode), - label = "", - ) { (granted, currentMode) -> - List { - if (granted) { - if (currentMode == Mode.Custom) { - RootProfileConfig( - fixedName = true, - profile = profile, - onProfileChange = onProfileChange, - ) - } - - return@List - } - - val modifyEnabled = currentMode == Mode.Custom - - SwitchItem( - checked = - if (modifyEnabled) { - profile.umountModules - } else { - KsuNative.isDefaultUmountModules() - }, - enabled = modifyEnabled, - onChange = { - onProfileChange( - profile.copy( - umountModules = it, - nonRootUseDefault = false, - ), - ) - }, - ) { - Title(R.string.settings_umount_modules_default) - Description(R.string.settings_umount_modules_default_desc) - } - } - } - - Spacer(Modifier.height(bottomBarPaddingValues.calculateBottomPadding())) - } - } - } - } - -private enum class Mode( - @StringRes private val res: Int, -) { - Default(R.string.profile_default), - Custom(R.string.profile_custom), - ; - - @get:Composable - val dialogOptions: List> - get() = - entries.map { - RadioDialogItem( - value = it, - title = it.text, - ) - } - - @get:Composable - val text: String get() = stringResource(res) -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/items/RootProfileConfig.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/items/RootProfileConfig.kt deleted file mode 100644 index 4252ab975..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/items/RootProfileConfig.kt +++ /dev/null @@ -1,344 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.appProfile.items - -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.core.text.isDigitsOnly -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ext.nullable -import com.dergoogler.mmrl.platform.SePolicy.isSepolicyValid -import com.dergoogler.mmrl.platform.ksu.Capabilities -import com.dergoogler.mmrl.platform.ksu.Groups -import com.dergoogler.mmrl.platform.ksu.Profile -import com.dergoogler.mmrl.platform.ksu.Profile.Namespace -import com.dergoogler.mmrl.ui.component.LabelItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.CheckboxDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.CheckboxItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.RadioDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Section -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.TextEditDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.DialogSupportingText -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title - -@Composable -fun ListScope.RootProfileConfig( - modifier: Modifier = Modifier, - fixedName: Boolean, - profile: Profile, - onProfileChange: (Profile) -> Unit, -) { - Section( - title = stringResource(R.string.profile_custom), - ) { - RadioDialogItem( - selection = profile.namespace, - options = NamespaceOptions, - onConfirm = { - onProfileChange(profile.copy(namespace = it.value)) - }, - ) { - Title(R.string.profile_namespace) - it.title.nullable { t -> - Description(t) - } - } - - UidPanel( - uid = profile.uid, - label = "User Identifier (UID)", - onUidChange = { - onProfileChange( - profile.copy( - uid = it, - rootUseDefault = false, - ), - ) - }, - ) - - UidPanel( - uid = profile.gid, - label = "Group Identifier (GID)", - onUidChange = { - onProfileChange( - profile.copy( - gid = it, - rootUseDefault = false, - ), - ) - }, - ) - - val selectedGroups = - profile.groups.ifEmpty { listOf(0) }.let { e -> - e.mapNotNull { g -> - Groups.entries.find { it.gid == g } - } - } - - GroupsPanel(selectedGroups) { - onProfileChange( - profile.copy( - groups = it.map { group -> group.gid }.ifEmpty { listOf(0) }, - rootUseDefault = false, - ), - ) - } - - val selectedCaps = - profile.capabilities.mapNotNull { e -> - Capabilities.entries.find { it.cap == e } - } - - CapsPanel(selectedCaps) { - onProfileChange( - profile.copy( - capabilities = it.map { cap -> cap.cap }, - rootUseDefault = false, - ), - ) - } - } - - Section( - stringResource(R.string.profile_selinux), - ) { - SELinuxPanel( - profile = profile, - onSELinuxChange = { domain, rules -> - onProfileChange( - profile.copy( - context = domain, - rules = rules, - rootUseDefault = false, - ), - ) - }, - ) - } -} - -@Composable -fun ListScope.GroupsPanel( - selected: List, - closeSelection: (selection: List) -> Unit, -) { - val groups = - remember(selected) { - Groups.entries.toTypedArray().sortedWith( - compareBy { if (selected.contains(it)) 0 else 1 } - .then( - compareBy { - when (it) { - Groups.ROOT -> 0 - Groups.SYSTEM -> 1 - Groups.SHELL -> 2 - else -> Int.MAX_VALUE - } - }, - ).then(compareBy { it.name }), - ) - } - - val options = - remember(groups) { - groups.map { value -> - CheckboxItem( - value = value, - title = value.display, - desc = value.desc, - checked = selected.contains(value), - ) - } - } - - CheckboxDialogItem( - strict = false, - multiple = true, - maxChoices = 32, - options = options, - onConfirm = { - closeSelection(it.map { item -> item.value }) - }, - ) { - Title(R.string.profile_groups) - Labels { - selected.forEach { - LabelItem(it.display) - } - } - } -} - -@Composable -fun ListScope.CapsPanel( - selected: Collection, - closeSelection: (selection: List) -> Unit, -) { - val caps = - remember(selected) { - Capabilities.entries.toTypedArray().sortedWith( - compareBy { if (selected.contains(it)) 0 else 1 } - .then(compareBy { it.name }), - ) - } - - val options = - remember(caps) { - caps.map { value -> - CheckboxItem( - value = value, - title = value.display, - desc = value.desc, - checked = selected.contains(value), - ) - } - } - - CheckboxDialogItem( - strict = false, - multiple = true, - options = options, - onConfirm = { - closeSelection(it.map { item -> item.value }) - }, - ) { - Title(R.string.profile_capabilities) - Labels { - selected.forEach { - LabelItem(it.display) - } - } - } -} - -@Composable -private fun ListScope.UidPanel( - uid: Int, - label: String, - onUidChange: (Int) -> Unit, -) { - val keyboardController = LocalSoftwareKeyboardController.current - - var lastValidUid by remember { - mutableIntStateOf(uid) - } - - TextEditDialogItem( - value = uid.toString(), - onValid = { - isTextValidUid(it) - }, - keyboardOptions = - KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done, - ), - keyboardActions = - KeyboardActions(onDone = { - keyboardController?.hide() - }), - onConfirm = { - if (it.isEmpty()) { - onUidChange(0) - return@TextEditDialogItem - } - val valid = isTextValidUid(it) - - val targetUid = if (valid) it.toInt() else lastValidUid - if (valid) { - lastValidUid = it.toInt() - } - - onUidChange(targetUid) - }, - ) { - Title(label) - Description(it.value) - if (it.isError) { - DialogSupportingText("Invalid Input!") - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ListScope.SELinuxPanel( - profile: Profile, - onSELinuxChange: (domain: String, rules: String) -> Unit, -) { - TextEditDialogItem( - value = profile.context, - strict = false, - onValid = { - it.matches(Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$")) - }, - onConfirm = { - onSELinuxChange(it, profile.rules) - }, - ) { - if (it.isError) { - DialogSupportingText(stringResource(R.string.profile_selinux_context_invalid)) - } - - Title(stringResource(R.string.profile_selinux_context)) - Description(it.value) - } - - TextEditDialogItem( - value = profile.rules, - strict = false, - onValid = { - isSepolicyValid(it) - }, - onConfirm = { - onSELinuxChange(profile.context, it) - }, - ) { - Title(stringResource(R.string.profile_selinux_rules)) - - if (it.isError) { - DialogSupportingText(stringResource(R.string.profile_selinux_rules_invalid)) - } - - if (it.value.isBlank()) { - Description(stringResource(R.string.empty)) - } else { - Description(it.value) - } - } -} - -@get:Composable -private val NamespaceOptions: List> - get() { - return enumValues().map { - val title = - when (it) { - Namespace.INHERITED -> stringResource(R.string.profile_namespace_inherited) - Namespace.GLOBAL -> stringResource(R.string.profile_namespace_global) - Namespace.INDIVIDUAL -> stringResource(R.string.profile_namespace_individual) - } - - RadioDialogItem( - value = it.ordinal, - title = title, - ) - } - } - -private fun isTextValidUid(text: String): Boolean = - text.isNotEmpty() && text.isDigitsOnly() && text.toInt() >= 0 && text.toInt() <= Int.MAX_VALUE diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/remember/rememberProfileChange.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/remember/rememberProfileChange.kt deleted file mode 100644 index aececf5c7..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/appProfile/remember/rememberProfileChange.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.appProfile.remember - -import android.util.Log -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.platform.SePolicy -import com.dergoogler.mmrl.platform.SePolicy.getSepolicy -import com.dergoogler.mmrl.platform.ksu.KsuNative -import com.dergoogler.mmrl.platform.ksu.Profile -import com.dergoogler.mmrl.ui.providable.LocalSnackbarHost -import com.dergoogler.mmrl.ui.providable.LocalSuperUserViewModel -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel -import kotlinx.coroutines.launch - -@Composable -fun rememberProfileChange(info: SuperUserViewModel.AppInfo): Pair Unit> { - val snackBarHost = LocalSnackbarHost.current - val scope = rememberCoroutineScope() - val viewModel = LocalSuperUserViewModel.current - - val initialProfile = - remember(info.packageName, info.uid) { - val profile = KsuNative.getAppProfile(info.packageName, info.uid) - if (profile.allowSu) { - profile.rules = getSepolicy(info.packageName) - } - profile - } - - var profile by rememberSaveable(info.packageName, info.uid) { - mutableStateOf(initialProfile) - } - - // Cache string resources to avoid recomposition - val errorMessages = - remember { - ErrorMessages( - failToUpdateAppProfile = R.string.failed_to_update_app_profile, - failToUpdateSepolicy = R.string.failed_to_update_sepolicy, - suNotAllowed = R.string.su_not_allowed, - ) - } - - val updateProfile: (Profile) -> Unit = - remember(info, snackBarHost, scope, viewModel) { - { newProfile -> - scope.launch { - try { - val result = - updateProfileSafely( - newProfile, - info, - errorMessages, - snackBarHost, - ) - if (result.isSuccess) { - profile = newProfile - viewModel.updateAppProfile(info.packageName, newProfile) - } - } catch (e: Exception) { - Log.e("ProfileChange", "Error updating profile", e) - snackBarHost.showSnackbar("An unexpected error occurred") - } - } - } - } - - return profile to updateProfile -} - -private data class ErrorMessages( - val failToUpdateAppProfile: Int, - val failToUpdateSepolicy: Int, - val suNotAllowed: Int, -) - -private sealed class UpdateResult { - object Success : UpdateResult() - - data class Error( - val messageRes: Int, - val args: List, - ) : UpdateResult() - - val isSuccess: Boolean get() = this is Success -} - -private suspend fun updateProfileSafely( - profile: Profile, - info: SuperUserViewModel.AppInfo, - errorMessages: ErrorMessages, - snackBarHost: SnackbarHostState, -): UpdateResult { - if (profile.allowSu) { - // Validate system UID restrictions - if (isSystemUidForbidden(info.uid)) { - snackBarHost.showSnackbar( - // You'll need to resolve string resource here or pass context - "SU not allowed for system UID ${info.uid}", - ) - return UpdateResult.Error(errorMessages.suNotAllowed, listOf(info.label)) - } - - // Handle SELinux policy updates - if (shouldUpdateSepolicy(profile)) { - val sepolicyResult = SePolicy.setSePolicy(profile.name, profile.rules) - if (!sepolicyResult) { - snackBarHost.showSnackbar("Failed to update SEPolicy for ${info.label}") - return UpdateResult.Error(errorMessages.failToUpdateSepolicy, listOf(info.label)) - } - } - } - - // Update app profile - val profileUpdateSuccess = KsuNative.setAppProfile(profile) - if (!profileUpdateSuccess) { - snackBarHost.showSnackbar("Failed to update app profile for ${info.label}") - return UpdateResult.Error(errorMessages.failToUpdateAppProfile, listOf(info.uid)) - } - - return UpdateResult.Success -} - -private fun isSystemUidForbidden(uid: Int): Boolean { - // sync with allowlist.c - forbid_system_uid - return uid < 2000 && uid != 1000 -} - -private fun shouldUpdateSepolicy(profile: Profile): Boolean = !profile.rootUseDefault && profile.rules.isNotEmpty() - -val LocalProfileChange = - staticCompositionLocalOf Unit>> { - error("CompositionLocal LocalProfileChange not present") - } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/exploreRepositories/ExploreRepositoryScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/exploreRepositories/ExploreRepositoryScreen.kt index 3018a18d5..4e3946f94 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/exploreRepositories/ExploreRepositoryScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/exploreRepositories/ExploreRepositoryScreen.kt @@ -70,6 +70,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import dev.chrisbanes.haze.hazeSource import dev.dergoogler.mmrl.compat.core.LocalUriHandler +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults import kotlinx.coroutines.launch @Destination @@ -81,7 +82,7 @@ fun ExploreRepositoryScreen(repo: ExploreRepository) = val scope = rememberCoroutineScope() val snackbarHostState = LocalSnackbarHost.current val browser = LocalUriHandler.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = ToolbarDefaults.pinnedScrollBehavior() val userPreferences = LocalUserPreferences.current val repositoriesMenu = userPreferences.repositoriesMenu diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/HomeScreen.kt index 35945cbf8..88a5a882e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/HomeScreen.kt @@ -7,26 +7,19 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable -import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,11 +33,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R import com.dergoogler.mmrl.datastore.model.WorkingMode.Companion.isNonRoot import com.dergoogler.mmrl.datastore.model.WorkingMode.Companion.isRoot -import com.dergoogler.mmrl.ext.currentScreenWidth import com.dergoogler.mmrl.ext.managerVersion import com.dergoogler.mmrl.ext.none import com.dergoogler.mmrl.ext.nullable @@ -56,35 +49,38 @@ import com.dergoogler.mmrl.platform.file.SuFile.Companion.toFormattedFileSize import com.dergoogler.mmrl.platform.ksu.KsuNative import com.dergoogler.mmrl.stub.IMMRLApiManager import com.dergoogler.mmrl.ui.component.Alert -import com.dergoogler.mmrl.ui.component.LocalScreenProvider +import com.dergoogler.mmrl.ui.component.BottomBar import com.dergoogler.mmrl.ui.component.SELinuxStatus import com.dergoogler.mmrl.ui.component.TopAppBarEventIcon -import com.dergoogler.mmrl.ui.component.card.Card -import com.dergoogler.mmrl.ui.component.card.component.Relative -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.component.toolbar.BlurToolbar import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.ui.remember.rememberLocalAnalytics import com.dergoogler.mmrl.ui.remember.seLinuxContext import com.dergoogler.mmrl.ui.remember.superUserCount import com.dergoogler.mmrl.ui.remember.versionName -import com.dergoogler.mmrl.ui.screens.home.items.NonRootItem -import com.dergoogler.mmrl.ui.screens.home.items.RebootBottomSheet -import com.dergoogler.mmrl.ui.screens.home.items.RootItem +import com.dergoogler.mmrl.ui.screens.home.components.NonRootItem +import com.dergoogler.mmrl.ui.screens.home.components.RebootBottomSheet +import com.dergoogler.mmrl.ui.screens.home.components.RootItem import com.dergoogler.mmrl.ui.screens.settings.changelogs.items.ChangelogBottomSheet import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination import com.ramcosta.composedestinations.generated.destinations.ThankYouScreenDestination -import dev.chrisbanes.haze.hazeSource +import dev.mmrlx.compose.layout.outlinedCard +import dev.mmrlx.compose.ui.LinearProgressIndicator +import dev.mmrlx.compose.ui.LocalTextStyle +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton +import dev.mmrlx.compose.ui.list.List +import dev.mmrlx.compose.ui.list.component.Item +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Icon +import dev.mmrlx.compose.ui.list.component.item.Title +import dev.mmrlx.compose.ui.scaffold.Scaffold +import dev.mmrlx.compose.ui.toolbar.Toolbar +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber @@ -92,383 +88,369 @@ import timber.log.Timber val listItemContentPaddingValues: PaddingValues = PaddingValues(vertical = 8.dp, horizontal = 25.dp) @Destination(start = true) -@OptIn(ExperimentalComposeApi::class) @Composable -fun HomeScreen() = - LocalScreenProvider { - val navigator = LocalDestinationsNavigator.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - val context = LocalContext.current - val userPreferences = LocalUserPreferences.current - val browser = LocalUriHandler.current - - val analytics by rememberLocalAnalytics() - - var openRebootSheet by remember { mutableStateOf(false) } - if (openRebootSheet) { - RebootBottomSheet( - onClose = { openRebootSheet = false }, - ) - } - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopBar( - onInfoClick = { - navigator.navigate(AboutScreenDestination) - }, - onHeartClick = { - navigator.navigate(ThankYouScreenDestination) - }, - onRebootClick = { - openRebootSheet = true - }, - scrollBehavior = scrollBehavior, - ) - }, - contentWindowInsets = WindowInsets.none, - ) { innerPadding -> - ResponsiveContent { - Column( - modifier = - Modifier - .hazeSource(state = LocalHazeState.current) - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(16.dp) - .padding(top = innerPadding.calculateTopPadding()), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - when { - userPreferences.workingMode.isRoot -> - RootItem( - developerMode = userPreferences.developerMode, - ) +fun HomeScreen() { + val navigator = LocalDestinationsNavigator.current + val scrollBehavior = ToolbarDefaults.fadePinnedScrollBehavior() + val context = LocalContext.current + val userPreferences = LocalUserPreferences.current + val browser = LocalUriHandler.current + + val analytics by rememberLocalAnalytics() + + var openRebootSheet by remember { mutableStateOf(false) } + if (openRebootSheet) { + RebootBottomSheet( + onClose = { openRebootSheet = false }, + ) + } - userPreferences.workingMode.isNonRoot -> - NonRootItem( - developerMode = userPreferences.developerMode, - ) - } + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + toolbar = { + TopBar0( + onInfoClick = { + navigator.navigate(AboutScreenDestination) + }, + onHeartClick = { + navigator.navigate(ThankYouScreenDestination) + }, + onRebootClick = { + openRebootSheet = true + }, + scrollBehavior = scrollBehavior, + ) + }, + bottomBar = { + BottomBar() + }, + contentWindowInsets = WindowInsets.none, + ) { + Column( + modifier = + Modifier + .scaffoldHazeSource() + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .scaffoldPadding() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + when { + userPreferences.workingMode.isRoot -> + RootItem() + + userPreferences.workingMode.isNonRoot -> + NonRootItem() + } - if (userPreferences.checkAppUpdates) { - var changelog by remember { mutableStateOf?>(null) } - LaunchedEffect(Unit) { - runRequest { - withContext(Dispatchers.IO) { - val api = IMMRLApiManager.build() - return@withContext api.changelog.execute() - } - }.onSuccess { list -> - changelog = list - }.onFailure { - Timber.e(it, "unable to get changelog") - } + if (userPreferences.checkAppUpdates) { + var changelog by remember { mutableStateOf?>(null) } + LaunchedEffect(Unit) { + runRequest { + withContext(Dispatchers.IO) { + val api = IMMRLApiManager.build() + return@withContext api.changelog.execute() } + }.onSuccess { list -> + changelog = list + }.onFailure { + Timber.e(it, "unable to get changelog") + } + } - changelog?.let { - val latest = it.first() + changelog?.let { + val latest = it.first() - var changelogSheet by remember { mutableStateOf(false) } - if (changelogSheet) { - ChangelogBottomSheet( - changelog = latest, - onClose = { changelogSheet = false }, - ) - } + var changelogSheet by remember { mutableStateOf(false) } + if (changelogSheet) { + ChangelogBottomSheet( + changelog = latest, + onClose = { changelogSheet = false }, + ) + } - AnimatedVisibility( - visible = - userPreferences.developerMode { devAlwaysShowUpdateAlert } || - ( + AnimatedVisibility( + visible = + userPreferences.developerMode { devAlwaysShowUpdateAlert } || + ( if (latest.preRelease) { userPreferences.checkAppUpdatesPreReleases && - latest.versionCode > context.managerVersion.second + latest.versionCode > context.managerVersion.second } else { latest.versionCode > context.managerVersion.second } - ), - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut(), - ) { - Alert( - onClick = { - changelogSheet = true - }, - backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, - title = stringResource(R.string.update_available), - message = - stringResource( - R.string.new_version_available, - latest.versionName, - ), - icon = R.drawable.cloud_download, + ), + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + Alert( + onClick = { + changelogSheet = true + }, + backgroundColor = MaterialTheme.colorScheme.tertiaryContainer, + title = stringResource(R.string.update_available), + message = + stringResource( + R.string.new_version_available, + latest.versionName, + ), + icon = R.drawable.cloud_download, + ) + } + } + } + + List( + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = listItemContentPaddingValues, + ) { + val scope = this + + Column( + modifier = Modifier + .outlinedCard() + .padding(vertical = 16.dp) + .fillMaxWidth() + ) { + val uname = Os.uname() + + scope.Item { + Title { + Text( + "DEV BUILD! EXPECT ERRORS AND BROKEN UI/UX", + style = LocalTextStyle.current.copy( + fontFamily = FontFamily.Monospace, + color = Color.Red ) - } + ) } } - List( - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = listItemContentPaddingValues, - ) { - val scope = this + scope.Item { + Icon(painter = painterResource(R.drawable.cookie_man)) + Title(R.string.kernel) + Description(uname.release) + } - Card( - modifier = Modifier.padding(vertical = 16.dp), - ) { - val uname = Os.uname() - Column( - modifier = Modifier.relative(), - ) { - scope.Item { - Icon(painter = painterResource(R.drawable.cookie_man)) - Title(R.string.kernel) - Description(uname.release) - } + scope.Item { + Icon(painter = painterResource(R.drawable.launcher_outline)) + Title(R.string.manager_version) + Description("${context.managerVersion.first} (${context.managerVersion.second})") + } - scope.Item { - Icon(painter = painterResource(R.drawable.launcher_outline)) - Title(R.string.manager_version) - Description("${context.managerVersion.first} (${context.managerVersion.second})") - } + scope.Item { + Icon(painter = painterResource(R.drawable.fingerprint)) + Title(R.string.fingerprint) + Description( + if (userPreferences.hideFingerprintInHome) { + stringResource(id = R.string.hidden) + } else { + Build.FINGERPRINT + }, + ) + } - scope.Item { - Icon(painter = painterResource(R.drawable.fingerprint)) - Title(R.string.fingerprint) - Description( - if (userPreferences.hideFingerprintInHome) { - stringResource(id = R.string.hidden) - } else { - Build.FINGERPRINT - }, - ) - } + scope.Item { + Icon(painter = painterResource(R.drawable.cpu_2)) + Title(R.string.architecture) + Description(uname.machine) + } - scope.Item { - Icon(painter = painterResource(R.drawable.cpu_2)) - Title(R.string.architecture) - Description(uname.machine) - } + scope.SELinuxStatus() - scope.SELinuxStatus() + PlatformManager.platform.isKernelSuOrNext.takeTrue { + scope.Item { + Icon(painter = painterResource(R.drawable.user_outlined)) + Title(R.string.super_user_apps) + Description(superUserCount.toString()) + } + } + } - PlatformManager.platform.isKernelSuOrNext.takeTrue { + PlatformManager.isAlive.takeTrue { + userPreferences.developerMode.takeTrue { + Column( + modifier = Modifier + .outlinedCard() + .padding(vertical = 16.dp) + .fillMaxWidth() + ) { + scope.Item { + Title(R.string.home_root_provider_version_name) + Description(versionName) + } + + scope.Item { + Title(R.string.home_root_provider_se_linux_context) + Description(seLinuxContext) + } + + if (PlatformManager.platform.isKernelSuNext) { + KsuNative.getHookMode().nullable { scope.Item { - Icon(painter = painterResource(R.drawable.user_outlined)) - Title(R.string.super_user_apps) - Description(superUserCount.toString()) + Title(R.string.hook_mode) + Description(it) } } } - } - PlatformManager.isAlive.takeTrue { - userPreferences.developerMode.takeTrue { - Card( - modifier = Modifier.padding(vertical = 16.dp), - ) { - Column( - modifier = Modifier.relative(), - ) { - scope.Item { - Title(R.string.home_root_provider_version_name) - Description(versionName) - } - - scope.Item { - Title(R.string.home_root_provider_se_linux_context) - Description(seLinuxContext) - } - - if (PlatformManager.platform.isKernelSuNext) { - KsuNative.getHookMode().nullable { - scope.Item { - Title(R.string.hook_mode) - Description(it) - } - } - } - - if (PlatformManager.platform.isSukiSU) { - KsuNative.getHookType().nullable { - scope.Item { - Title(R.string.hook_mode) - Description(it) - } - } - - scope.Item { - Title(R.string.kpm_support) - Description(if (KsuNative.isKPMEnabled()) R.string.yes else R.string.no) - } - } + if (PlatformManager.platform.isSukiSU) { + KsuNative.getHookType().nullable { + scope.Item { + Title(R.string.hook_mode) + Description(it) } } + + scope.Item { + Title(R.string.kpm_support) + Description(if (KsuNative.isKPMEnabled()) R.string.yes else R.string.no) + } } } + } + } - if (PlatformManager.platform.isValid) { - analytics.nullable { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Card( - modifier = - Modifier - .padding(vertical = 16.dp) - .weight(1f), - ) { - Relative { - scope.Item { - Title(R.string.home_installed_modules) - Description(it.totalModules.toString()) - } - } - } - - Card( - modifier = - Modifier - .padding(vertical = 16.dp) - .weight(1f), - ) { - Relative { - scope.Item { - Title(R.string.home_updated_modules) - Description(it.totalUpdated.toString()) - } - } - } + if (PlatformManager.platform.isValid) { + analytics.nullable { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column( + modifier = Modifier + .outlinedCard() + .padding(vertical = 16.dp) + .weight(1f) + ) { + scope.Item { + Title(R.string.home_installed_modules) + Description(it.totalModules.toString()) } + } - Card( - modifier = - Modifier - .padding(vertical = 16.dp), - ) { - Relative { - scope.Item { - Title(R.string.home_storage_usage) - - Description { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = it.totalModulesUsageBytes.toFormattedFileSize(), - ) - - LinearProgressIndicator( - progress = { - it.totalStorageUsage - }, - modifier = - Modifier - .height(10.dp) - .weight(1f), - drawStopIndicator = {}, - ) - - Text( - text = it.totalDeviceStorageBytes.toFormattedFileSize(), - ) - } - } - } - } + Column( + modifier = Modifier + .outlinedCard() + .padding(vertical = 16.dp) + .weight(1f) + ) { + scope.Item { + Title(R.string.home_updated_modules) + Description(it.totalUpdated.toString()) } + } + } - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Card( - modifier = - Modifier - .padding(vertical = 16.dp) - .weight(1f), - ) { - Relative { - scope.Item { - Title(R.string.home_enabled_modules) - Description(it.totalEnabled.toString()) - } - } - } + Column( + modifier = Modifier + .outlinedCard() + .padding(vertical = 16.dp) + .fillMaxWidth() + ) { + scope.Item { + Title(R.string.home_storage_usage) - Card( - modifier = - Modifier - .padding(vertical = 16.dp) - .weight(1f), + Description { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Relative { - scope.Item { - Title(R.string.home_disabled_modules) - Description(it.totalDisabled.toString()) - } - } + Text( + text = it.totalModulesUsageBytes.toFormattedFileSize(), + ) + + LinearProgressIndicator( + progress = { + it.totalStorageUsage + }, + modifier = + Modifier + .height(10.dp) + .weight(1f), + drawStopIndicator = {}, + ) + + Text( + text = it.totalDeviceStorageBytes.toFormattedFileSize(), + ) } } } } - Card( - modifier = - Modifier - .padding(vertical = 16.dp) - .fillMaxWidth(), - onClick = { - browser.openUri("https://github.com/sponsors/MMRLApp") - }, + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - Relative { + Column( + modifier = Modifier + .outlinedCard() + .padding(vertical = 16.dp) + .weight(1f) + ) { + scope.Item { + Title(R.string.home_enabled_modules) + Description(it.totalEnabled.toString()) + } + } + + Column( + modifier = Modifier + .outlinedCard() + .padding(vertical = 16.dp) + .weight(1f) + ) { scope.Item { - Title( - id = R.string.home_support_title, - styleTransform = { - val newStyle = it.copy(color = Color.Unspecified) - it.merge(newStyle) - }, - ) - - Description( - id = R.string.home_support_content, - styleTransform = { - val newStyle = it.copy(color = Color.Unspecified) - it.merge(newStyle) - }, - ) + Title(R.string.home_disabled_modules) + Description(it.totalDisabled.toString()) } } } } + } - val paddingValues = LocalMainScreenInnerPaddings.current - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) + Column( + modifier = Modifier + .outlinedCard() + .fillMaxWidth() + .clickable { + browser.openUri("https://github.com/sponsors/MMRLApp") + } + .padding(vertical = 16.dp) + ) { + scope.Item { + Title( + id = R.string.home_support_title, + styleTransform = { + val newStyle = it.copy(color = Color.Unspecified) + it.merge(newStyle) + }, + ) + + Description( + id = R.string.home_support_content, + styleTransform = { + val newStyle = it.copy(color = Color.Unspecified) + it.merge(newStyle) + }, + ) + } } } } } +} @Composable -private fun TopBar( +private fun TopBar0( onRebootClick: () -> Unit = {}, onInfoClick: () -> Unit = {}, onHeartClick: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior, + scrollBehavior: ToolbarScrollBehavior, ) { - val width = currentScreenWidth() - - BlurToolbar( + Toolbar( scrollBehavior = scrollBehavior, - fade = true, - fadeDistance = 50f, title = { - if (!width.isSmall) return@BlurToolbar - TopAppBarEventIcon() }, actions = { diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/NonRootItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/NonRootItem.kt similarity index 67% rename from app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/NonRootItem.kt rename to app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/NonRootItem.kt index 56c03f1b5..7e9f17e94 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/NonRootItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/NonRootItem.kt @@ -1,5 +1,6 @@ -package com.dergoogler.mmrl.ui.screens.home.items +package com.dergoogler.mmrl.ui.screens.home.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -16,27 +18,36 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ext.takeTrue -import com.dergoogler.mmrl.ui.component.card.Card +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import dev.mmrlx.compose.ui.scaffold.PositionLayout +import dev.mmrlx.compose.ui.theme.MMRLXTheme + @Composable -internal fun NonRootItem(developerMode: Boolean = false) = - Card( - color = MaterialTheme.colorScheme.secondaryContainer, +internal fun NonRootItem() { + val userPreferences = LocalUserPreferences.current + val shape = MMRLXTheme.shapes.extraLarge + + PositionLayout( + modifier = Modifier + .clip(shape) + .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)) ) { - developerMode.takeTrue { + if (userPreferences.developerMode) { Surface( shape = RoundedCornerShape( - topEnd = 20.dp, - bottomStart = 15.dp, - bottomEnd = 0.dp, + topStart = CornerSize(0.dp), + topEnd = shape.topEnd, + bottomStart = CornerSize(15.dp), + bottomEnd = CornerSize(0.dp), ), - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.45f), modifier = Modifier .absolute(Alignment.TopEnd), @@ -44,7 +55,7 @@ internal fun NonRootItem(developerMode: Boolean = false) = Text( text = "USER!DEV", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary, + color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp), @@ -53,14 +64,16 @@ internal fun NonRootItem(developerMode: Boolean = false) = } Row( - modifier = Modifier.relative().padding(20.dp), + modifier = Modifier + .relative() + .padding(20.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.size(45.dp), painter = painterResource(id = R.drawable.info_circle_filled), contentDescription = null, - tint = MaterialTheme.colorScheme.primary, + tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), ) Spacer(modifier = Modifier.width(16.dp)) @@ -82,3 +95,4 @@ internal fun NonRootItem(developerMode: Boolean = false) = } } } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/RebootBottomSheet.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/RebootBottomSheet.kt similarity index 98% rename from app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/RebootBottomSheet.kt rename to app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/RebootBottomSheet.kt index 47318ff42..c56e288a6 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/RebootBottomSheet.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/RebootBottomSheet.kt @@ -1,4 +1,4 @@ -package com.dergoogler.mmrl.ui.screens.home.items +package com.dergoogler.mmrl.ui.screens.home.components import android.content.Context import android.os.Build diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/RootItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/RootItem.kt similarity index 64% rename from app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/RootItem.kt rename to app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/RootItem.kt index 15d9516f2..d444da02e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/items/RootItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/home/components/RootItem.kt @@ -1,5 +1,6 @@ -package com.dergoogler.mmrl.ui.screens.home.items +package com.dergoogler.mmrl.ui.screens.home.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -16,42 +18,43 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ext.nullable -import com.dergoogler.mmrl.ext.takeTrue import com.dergoogler.mmrl.model.local.FeaturedManager import com.dergoogler.mmrl.model.local.FeaturedManager.Companion.name -import com.dergoogler.mmrl.platform.PlatformManager.isAlive -import com.dergoogler.mmrl.platform.PlatformManager.platform -import com.dergoogler.mmrl.ui.component.LabelItem -import com.dergoogler.mmrl.ui.component.card.Card -import com.dergoogler.mmrl.ui.component.text.TextRow +import com.dergoogler.mmrl.platform.PlatformManager import com.dergoogler.mmrl.ui.providable.LocalUserPreferences -import com.dergoogler.mmrl.ui.remember.isLkmMode import com.dergoogler.mmrl.ui.remember.versionCode +import dev.mmrlx.compose.ui.scaffold.PositionLayout +import dev.mmrlx.compose.ui.theme.MMRLXTheme @Composable -internal fun RootItem(developerMode: Boolean = false) { +internal fun RootItem() { val userPreferences = LocalUserPreferences.current + val shape = MMRLXTheme.shapes.extraLarge + val isAlive = PlatformManager.isAlive val manager = FeaturedManager.managers.find { userPreferences.workingMode == it.workingMode } - Card( - color = MaterialTheme.colorScheme.secondaryContainer, + PositionLayout( + modifier = Modifier + .clip(shape) + .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)) ) { - developerMode.takeTrue { + if (userPreferences.developerMode) { Surface( shape = RoundedCornerShape( - topEnd = 20.dp, - bottomStart = 15.dp, - bottomEnd = 0.dp, + topStart = CornerSize(0.dp), + topEnd = shape.topEnd, + bottomStart = CornerSize(15.dp), + bottomEnd = CornerSize(0.dp), ), - color = MaterialTheme.colorScheme.primary, + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.45f), modifier = Modifier .absolute(Alignment.TopEnd), @@ -59,7 +62,7 @@ internal fun RootItem(developerMode: Boolean = false) { Text( text = "USER!DEV", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary, + color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp), @@ -89,45 +92,33 @@ internal fun RootItem(developerMode: Boolean = false) { modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - TextRow( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - leadingContent = - (developerMode && platform.isKernelSuOrNext) nullable { - LabelItem( - text = - when (isLkmMode) { - null -> "LTS" - true -> "LKM" - else -> "GKI" - }, + Text( + text = + if (isAlive) { + stringResource( + id = R.string.settings_root_access, + stringResource(id = R.string.settings_root_granted), + ) + } else { + stringResource( + id = R.string.settings_root_access, + stringResource(id = R.string.settings_root_none), ) }, - ) { - Text( - text = - if (isAlive) { - stringResource( - id = R.string.settings_root_access, - stringResource(id = R.string.settings_root_granted), - ) - } else { - stringResource( - id = R.string.settings_root_access, - stringResource(id = R.string.settings_root_none), - ) - }, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer, - ) - } + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) Text( text = if (isAlive) { stringResource( id = R.string.settings_root_provider, - manager.name, + if (manager?.name == null) { + "???" + } else { + manager.name + }, ) } else { stringResource( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/main/MainScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/main/MainScreen.kt index fc8cd65ac..bc28015d4 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/main/MainScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/main/MainScreen.kt @@ -6,26 +6,8 @@ import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationDrawerItem -import androidx.compose.material3.NavigationRail -import androidx.compose.material3.NavigationRailItem -import androidx.compose.material3.PermanentDrawerSheet -import androidx.compose.material3.PermanentNavigationDrawer import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -33,20 +15,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavBackStackEntry -import com.dergoogler.mmrl.ext.currentScreenWidth import com.dergoogler.mmrl.ext.none -import com.dergoogler.mmrl.ui.component.TopAppBar -import com.dergoogler.mmrl.ui.component.TopAppBarEventIcon -import com.dergoogler.mmrl.ui.component.scaffold.ResponsiveScaffold -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.component.toolbar.BlurBottomToolbar -import com.dergoogler.mmrl.ui.navigation.MainDestination -import com.dergoogler.mmrl.ui.navigation.isAccessible import com.dergoogler.mmrl.ui.providable.LocalBulkInstall import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalHazeState @@ -62,11 +33,10 @@ import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationSty import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.NavGraphs -import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.rememberHazeState +import dev.mmrlx.compose.ui.scaffold.Scaffold -@Destination @Composable fun MainScreen() { // val width = currentScreenWidth() @@ -89,90 +59,11 @@ fun MainScreen() { LocalSnackbarHost provides snackbarHostState, LocalBulkInstall provides bulkInstallViewModel, ) { - /*if (width.isLarge) { - Scaffold( - contentWindowInsets = WindowInsets.none, - ) { paddingValues -> - val navController = LocalNavController.current - - PermanentNavigationDrawer( - drawerContent = { - PermanentDrawerSheet( - modifier = - Modifier - .width(240.dp), - ) { - TopAppBar( - title = { - TopAppBarEventIcon() - }, - ) - - LazyColumn( - contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - items( - items = MainDestination.entries, - key = { it.name }, - ) { screen -> - val isSelected by navController.isRouteOnBackStackAsState(screen.direction) - - if (!screen.isAccessible) return@items - - NavigationDrawerItem( - icon = { - BaseNavIcon(screen, isSelected, updates) - }, - label = { - Text( - text = stringResource(id = screen.label), - style = MaterialTheme.typography.labelLarge, - ) - }, - selected = isSelected, - onClick = { - if (isSelected) { - navigator.popBackStack(screen.direction, false) - } - navigator.navigate(screen.direction) { - popUpTo(NavGraphs.root) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - ) - } - } - } - }, - ) { - CompositionLocalProvider( - LocalMainScreenInnerPaddings provides paddingValues, - ) { - CurrentNavHost( - Modifier.padding(paddingValues), - ) - } - } - } - - return@CompositionLocalProvider - }*/ - - ResponsiveScaffold( - bottomBar = { - BottomNav(updates) - }, - railBar = { -// RailNav(updates) - }, + Scaffold( contentWindowInsets = WindowInsets.none, - ) { paddingValues -> + ) { CompositionLocalProvider( - LocalMainScreenInnerPaddings provides paddingValues, + LocalMainScreenInnerPaddings provides contentPadding, ) { CurrentNavHost( modifier = Modifier.hazeSource(hazeState), @@ -199,137 +90,3 @@ private fun CurrentNavHost(modifier: Modifier = Modifier) { ) } -@Composable -private fun BottomNav(updates: Int) { - val prefs = LocalUserPreferences.current - val navigator = LocalDestinationsNavigator.current - val navController = LocalNavController.current - - BlurBottomToolbar( - modifier = - Modifier - .imePadding(), - ) { - MainDestination.entries.forEach { screen -> - val isSelected by navController.isRouteOnBackStackAsState(screen.direction) - - if (!screen.isAccessible) return@forEach - - NavigationBarItem( - icon = { - BaseNavIcon(screen, isSelected, updates) - }, - label = { - Text( - text = stringResource(id = screen.label), - style = MaterialTheme.typography.labelLarge, - ) - }, - alwaysShowLabel = !prefs.hideBottomBarLabels, - selected = isSelected, - onClick = { - if (isSelected) { - navigator.popBackStack(screen.direction, false) - } - navigator.navigate(screen.direction) { - popUpTo(NavGraphs.root) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - ) - } - } -} -/* -@Composable -private fun RailNav(updates: Int) { - val prefs = LocalUserPreferences.current - val navigator = LocalDestinationsNavigator.current - val navController = LocalNavController.current - - NavigationRail( - header = { - TopAppBarEventIcon() - }, - ) { - MainDestination.entries.forEach { screen -> - val isSelected by navController.isRouteOnBackStackAsState(screen.direction) - - if (!screen.isAccessible) return@forEach - - NavigationRailItem( - icon = { - BaseNavIcon(screen, isSelected, updates) - }, - label = { - Text( - text = stringResource(id = screen.label), - style = MaterialTheme.typography.labelLarge, - ) - }, - alwaysShowLabel = !prefs.hideBottomBarLabels, - selected = isSelected, - onClick = { - if (isSelected) { - navigator.popBackStack(screen.direction, false) - } - navigator.navigate(screen.direction) { - popUpTo(NavGraphs.root) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - ) - } - } -}*/ - -@Composable -private fun BaseNavIcon( - screen: MainDestination, - selected: Boolean, - updates: Int, -) { - if (screen == MainDestination.Modules && updates > 0) { - BadgedBox( - badge = { - Badge { - Text(text = updates.toString()) - } - }, - ) { - Icon( - painter = - painterResource( - id = - if (selected) { - screen.iconFilled - } else { - screen.icon - }, - ), - contentDescription = null, - ) - } - - return - } - - Icon( - painter = - painterResource( - id = - if (selected) { - screen.iconFilled - } else { - screen.icon - }, - ), - contentDescription = null, - ) -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/main/RootScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/main/RootScreen.kt deleted file mode 100644 index f8b1303fe..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/main/RootScreen.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.main - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavBackStackEntry -import com.dergoogler.mmrl.ext.none -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.providable.LocalBulkInstall -import com.dergoogler.mmrl.ui.providable.LocalNavController -import com.dergoogler.mmrl.ui.providable.LocalSnackbarHost -import com.dergoogler.mmrl.ui.providable.LocalUserPreferences -import com.dergoogler.mmrl.utils.initPlatform -import com.dergoogler.mmrl.viewmodel.BulkInstallViewModel -import com.ramcosta.composedestinations.DestinationsNavHost -import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle -import com.ramcosta.composedestinations.generated.NavGraphs - -@Composable -fun RootScreen() { - val navController = LocalNavController.current - val snackbarHostState = remember { SnackbarHostState() } - val bulkInstallViewModel: BulkInstallViewModel = hiltViewModel() - val context = LocalContext.current - val userPreferences = LocalUserPreferences.current - - LaunchedEffect(Unit) { - initPlatform(context, userPreferences.workingMode.toPlatform()) - } - - Scaffold( - snackbarHost = { SnackbarHost(snackbarHostState) }, - contentWindowInsets = WindowInsets.none, - ) { paddingValues -> - CompositionLocalProvider( - LocalSnackbarHost provides snackbarHostState, - LocalBulkInstall provides bulkInstallViewModel, - ) { - DestinationsNavHost( - modifier = Modifier.padding(bottom = paddingValues.calculateBottomPadding()), - navGraph = NavGraphs.root, - navController = navController, - defaultTransitions = - object : NavHostAnimatedDestinationStyle() { - override val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition - get() = { fadeIn(animationSpec = tween(340)) } - override val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition - get() = { fadeOut(animationSpec = tween(340)) } - }, - ) - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/DescriptionScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/DescriptionScreen.kt new file mode 100644 index 000000000..8da2090a8 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/DescriptionScreen.kt @@ -0,0 +1,176 @@ +package com.dergoogler.mmrl.ui.screens.moduleView + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.BuildConfig +import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.ext.none +import com.dergoogler.mmrl.model.WebColors +import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.dergoogler.mmrl.utils.MimeUtil +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.mmrlx.compose.ui.ext.with +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton +import dev.mmrlx.compose.ui.scaffold.Scaffold +import dev.mmrlx.compose.ui.theme.MMRLXTheme +import dev.mmrlx.compose.ui.toolbar.Toolbar +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior +import dev.mmrlx.compose.ui.toolbar.ToolbarTitle +import dev.mmrlx.compose.webui.WebUIView +import dev.mmrlx.compose.webui.insets +import dev.mmrlx.compose.webui.rememberWebUIState +import dev.mmrlx.webui.RouteRegistry +import dev.mmrlx.webui.RouteScope +import dev.mmrlx.webui.WebResourceResponse +import dev.mmrlx.webui.WebUIOptions +import java.net.HttpURLConnection +import java.net.URL + +@Composable +@Destination +fun DescriptionScreen(readmeUrl: String) { + val navigator = LocalDestinationsNavigator.current + val scrollBehavior = ToolbarDefaults.pinnedScrollBehavior() + val userPrefs = LocalUserPreferences.current + + val colors = MMRLXTheme.colors + val scheme = MaterialTheme.colorScheme.copy( + background = colors.background, + onBackground = colors.foreground, + surface = colors.code, + surfaceTint = colors.muted + ) + + val wstate = + rememberWebUIState( + "https://desc.mmrl.dev", + WebUIOptions("internal/assets/markdown.html") + ) { + it + .settings { + debug = BuildConfig.DEBUG + forceKillProcess = false + useConsoleInterceptor = false + darkMode = userPrefs.isDarkMode() + } + .client { + // not related to client + it.webview.setBackgroundColor(scheme.background.toArgb()) + } + .chromeClient { } + .registerRoutes { + val webColors = WebColors(scheme) + + internalRoute("readme.md") { + val connection = URL(readmeUrl).openConnection() as HttpURLConnection + connection.connect() + htmlResponse(connection.getInputStream()) + } + + internalRoute("insets.css") { + insets.css.asStyleResponse() + } + + internalRoute("colors.css") { + webColors.allCssColors.asStyleResponse() + } + + internalRoute("assets/*asset") { + val filePath = + request.params["asset"] ?: return@internalRoute notFoundResponse + + try { + val inputStream = kontext.assets.open(filePath) + val mimeType = MimeUtil.getMimeFromFileName(filePath) + response(mimeType = mimeType, data = inputStream) + } catch (e: Exception) { + console.error("Failed to open $filePath from the assets: $e") + notFoundResponse + } + } + } + } + + DisposableEffect(Unit) { + onDispose { + wstate.destroy() + } + } + + // We have to override otherwise the WebUI will close the entire app + BackHandler { + if (wstate.canGoBack()) { + wstate.goBack() + return@BackHandler + } + + navigator.popBackStack() + } + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + toolbar = { + TopBar0( + scrollBehavior = scrollBehavior, + navigator = navigator, + ) + }, + contentWindowInsets = WindowInsets.none, + ) { + wstate.insets( + PaddingValues( + top = contentPadding.calculateTopPadding(), + bottom = with(LocalDensity.current) { WindowInsets.navigationBars.getBottom(this).dp }, + ) + ) + + WebUIView( + modifier = Modifier + .with(this@Scaffold) { + it.scaffoldHazeSource() + } + .fillMaxSize(), + state = wstate + ) + } +} + +@Composable +private fun TopBar0( + navigator: DestinationsNavigator, + scrollBehavior: ToolbarScrollBehavior, +) = Toolbar( + navigationIcon = { + IconButton(onClick = { navigator.popBackStack() }) { + Icon( + painter = painterResource(id = R.drawable.arrow_left), + contentDescription = null, + ) + } + }, + title = { ToolbarTitle(title = stringResource(id = R.string.view_module_about_this_module)) }, + scrollBehavior = scrollBehavior, +) + +private fun RouteRegistry.internalRoute( + name: String, + content: RouteScope.() -> WebResourceResponse, +) = route("internal/$name", content) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/NewViewScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/NewViewScreen.kt index 592b389c2..4f7749f20 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/NewViewScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/NewViewScreen.kt @@ -6,16 +6,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -37,7 +33,6 @@ import com.dergoogler.mmrl.ext.fadingEdge import com.dergoogler.mmrl.ext.ifNotEmpty import com.dergoogler.mmrl.ext.none import com.dergoogler.mmrl.ext.nullable -import com.dergoogler.mmrl.ext.systemBarsPaddingEnd import com.dergoogler.mmrl.model.local.BulkModule import com.dergoogler.mmrl.model.online.OnlineModule import com.dergoogler.mmrl.model.online.VersionItem @@ -45,11 +40,9 @@ import com.dergoogler.mmrl.model.online.isBlacklisted import com.dergoogler.mmrl.ui.activity.terminal.install.InstallActivity import com.dergoogler.mmrl.ui.component.Cover import com.dergoogler.mmrl.ui.component.LocalScreenProvider -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold import com.dergoogler.mmrl.ui.providable.LocalBulkInstall import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings import com.dergoogler.mmrl.ui.providable.LocalModule import com.dergoogler.mmrl.ui.providable.LocalOnlineModule import com.dergoogler.mmrl.ui.providable.LocalRepo @@ -68,11 +61,15 @@ import com.dergoogler.mmrl.ui.screens.moduleView.sections.Header import com.dergoogler.mmrl.ui.screens.moduleView.sections.Information import com.dergoogler.mmrl.ui.screens.moduleView.sections.Information0 import com.dergoogler.mmrl.ui.screens.moduleView.sections.Screenshots -import com.dergoogler.mmrl.ui.screens.moduleView.sections.Toolbar +import com.dergoogler.mmrl.ui.screens.moduleView.sections.Toolbar0 import com.dergoogler.mmrl.viewmodel.ModuleViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph -import dev.chrisbanes.haze.hazeSource +import dev.mmrlx.compose.ui.HorizontalDivider +import dev.mmrlx.compose.ui.LinearProgressIndicator +import dev.mmrlx.compose.ui.ext.with +import dev.mmrlx.compose.ui.scaffold.Scaffold +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults import timber.log.Timber internal val listItemContentPaddingValues = PaddingValues(vertical = 16.dp, horizontal = 16.dp) @@ -181,7 +178,7 @@ fun NewViewScreen( ) } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = ToolbarDefaults.fadePinnedScrollBehavior() val isBlacklisted by module.isBlacklisted @@ -218,98 +215,94 @@ fun NewViewScreen( Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), snackbarHost = { SnackbarHost(snackbarHostState) }, - topBar = { - Toolbar() + toolbar = { + Toolbar0() }, contentWindowInsets = WindowInsets.none, - ) { innerPadding -> - this@Scaffold.ResponsiveContent { - Column( - modifier = - Modifier - .let { - if (repositoryMenu.showCover && module.hasCover) { - Modifier - } else { - it.padding(innerPadding) - } - }.verticalScroll(rememberScrollState()) - .hazeSource(state = hazeState), - ) { - module.cover.nullable(repositoryMenu.showCover) { - if (it.isNotEmpty()) { - Cover( - modifier = - Modifier.fadingEdge( - Brush.verticalGradient( - colors = - listOf( - Color.Transparent, - Color.Black, - ), - startY = Float.POSITIVE_INFINITY, - endY = 0f, - ), - ), - url = it, - ) - } - } - - Column( - modifier = - Modifier - .systemBarsPaddingEnd(), - ) { - Spacer(modifier = Modifier.height(16.dp)) + ) { + val scope = this - Header() - - val progress = - lastVersionItem?.let { - viewModel.getProgress(it) - } ?: 0f - - if (progress != 0f) { - LinearProgressIndicator( - progress = { progress }, - strokeCap = StrokeCap.Round, - modifier = - Modifier - .padding(vertical = 16.dp) - .height(0.9.dp) - .fillMaxWidth(), - ) - } else { - HorizontalDivider( - modifier = Modifier.padding(vertical = 16.dp), - thickness = 0.9.dp, - ) + Column( + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .scaffoldHazeSource() + .let { + if (repositoryMenu.showCover && module.hasCover) { + it + } else { + it.padding(top = scaffoldTopPadding) + } } + ) { + module.cover.nullable(repositoryMenu.showCover) { + if (it.isNotEmpty()) { + Cover( + modifier = + Modifier.fadingEdge( + Brush.verticalGradient( + colors = + listOf( + Color.Transparent, + Color.Black, + ), + startY = Float.POSITIVE_INFINITY, + endY = 0f, + ), + ), + url = it, + ) + } + } - Alerts() - - AboutModule() - - Screenshots() + Column( + modifier = Modifier.with(scope) { + it.scaffoldBottomPadding() + } + ) { + Spacer(modifier = Modifier.height(16.dp)) - Spacer(modifier = Modifier.height(16.dp)) + Header() - Information0() + val progress = + lastVersionItem?.let { + viewModel.getProgress(it) + } ?: 0f - // Information section + if (progress != 0f) { + LinearProgressIndicator( + progress = { progress }, + strokeCap = StrokeCap.Round, + modifier = + Modifier + .padding(vertical = 16.dp) + .height(0.9.dp) + .fillMaxWidth(), + ) + } else { HorizontalDivider( modifier = Modifier.padding(vertical = 16.dp), thickness = 0.9.dp, ) + } - Information() + Alerts() - Spacer(modifier = Modifier.navigationBarsPadding()) + AboutModule() - val paddingValues = LocalMainScreenInnerPaddings.current - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) - } + Screenshots() + + Spacer(modifier = Modifier.height(16.dp)) + + Information0() + + // Information section + HorizontalDivider( + modifier = Modifier.padding(vertical = 16.dp), + thickness = 0.9.dp, + ) + + Information() } } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/ViewDescriptionScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/ViewDescriptionScreen.kt deleted file mode 100644 index c9d93c30b..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/ViewDescriptionScreen.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.moduleView - -import android.annotation.SuppressLint -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.dergoogler.mmrl.BuildConfig -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ext.none -import com.dergoogler.mmrl.pathHandler.InternalPathHandler -import com.dergoogler.mmrl.ui.component.LocalScreenProvider -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.component.toolbar.BlurToolbar -import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings -import com.dergoogler.mmrl.ui.providable.LocalUserPreferences -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import dev.chrisbanes.haze.hazeSource -import dev.mmrlx.compose.webui.WebUIView -import dev.mmrlx.compose.webui.insets -import dev.mmrlx.compose.webui.rememberWebUIState - -const val launchUrl = "https://mui.kernelsu.org/internal/assets/markdown.html" - -@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") -@Composable -@Destination -fun ViewDescriptionScreen(readmeUrl: String) = - LocalScreenProvider { - val density = LocalDensity.current - val navigator = LocalDestinationsNavigator.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - val userPrefs = LocalUserPreferences.current - val scheme = MaterialTheme.colorScheme - - @Suppress("KotlinConstantConditions") val wstate = - rememberWebUIState("https://desc.mmrl.dev", "internal/assets/markdown.html") { - it - .settings { - schemeWhitelist += "ksu" - useDefaultApplicationInterface = false - useDefaultFileSystem = false - debug = BuildConfig.DEBUG - forceKillProcess = false - useConsoleInterceptor = false - darkMode = userPrefs.isDarkMode() - } - .client { - // not related to client - it.webview.setBackgroundColor(scheme.background.toArgb()) - } - .chromeClient { } - .registerPathHandler( - InternalPathHandler::class.java - ) { - add( - String::class.java to readmeUrl - ) - add( - ColorScheme::class.java to scheme - ) - } - } - - DisposableEffect(Unit) { - onDispose { - wstate.destroy() - } - } - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopBar( - scrollBehavior = scrollBehavior, - navigator = navigator, - ) - }, - contentWindowInsets = WindowInsets.none, - ) { innerPadding -> - val bottomBarPaddingValues = LocalMainScreenInnerPaddings.current - wstate.insets( - PaddingValues( - top = innerPadding.calculateTopPadding(), - bottom = bottomBarPaddingValues.calculateBottomPadding(), - ) - ) - - Column( - modifier = - Modifier - .fillMaxSize() - .hazeSource(LocalHazeState.current), - ) { - this@Scaffold.ResponsiveContent { - WebUIView(wstate) - } - } - } - } - -@Composable -private fun TopBar( - navigator: DestinationsNavigator, - scrollBehavior: TopAppBarScrollBehavior, -) = BlurToolbar( - navigationIcon = { - IconButton(onClick = { navigator.popBackStack() }) { - Icon( - painter = painterResource(id = R.drawable.arrow_left), - contentDescription = null, - ) - } - }, - title = { Text(text = stringResource(id = R.string.view_module_about_this_module)) }, - scrollBehavior = scrollBehavior, -) \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/items/OtherSourcesItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/items/OtherSourcesItem.kt index 0d861b49c..e32115c14 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/items/OtherSourcesItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/items/OtherSourcesItem.kt @@ -13,12 +13,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.model.online.OtherSources +import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalOnlineModule import com.dergoogler.mmrl.ui.providable.LocalOnlineModuleState import com.dergoogler.mmrl.ui.screens.repository.ModuleItemCompact +import com.ramcosta.composedestinations.generated.destinations.NewViewScreenDestination @Composable fun OtherSourcesItem(items: List) { + val navigator = LocalDestinationsNavigator.current val pagerState = rememberPagerState(pageCount = { (items.size + 2) / 3 }) HorizontalPager( @@ -49,7 +52,11 @@ fun OtherSourcesItem(items: List) { sourceProvider = item.repo.name, showLabels = false, showLastUpdated = false, - onClick = {}, + onClick = { + navigator.navigate( + NewViewScreenDestination(item.repo, item.online) + ) + }, ) } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/AboutModule.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/AboutModule.kt index f5ecbc3b4..fcb4b7986 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/AboutModule.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/AboutModule.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.AssistChip import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -19,19 +18,18 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R import com.dergoogler.mmrl.model.online.hasCategories -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListItemSlot -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalOnlineModule import com.dergoogler.mmrl.ui.providable.LocalRepo import com.dergoogler.mmrl.ui.screens.moduleView.listItemContentPaddingValues -import com.dergoogler.mmrl.ui.screens.repository.modules.ModulesFilter -import com.ramcosta.composedestinations.generated.destinations.TypedModulesScreenDestination -import com.ramcosta.composedestinations.generated.destinations.ViewDescriptionScreenDestination +import com.ramcosta.composedestinations.generated.destinations.DescriptionScreenDestination +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.list.List +import dev.mmrlx.compose.ui.list.ListItemSlot +import dev.mmrlx.compose.ui.list.component.ButtonItem +import dev.mmrlx.compose.ui.list.component.Item +import dev.mmrlx.compose.ui.list.component.item.Icon +import dev.mmrlx.compose.ui.list.component.item.Title @Composable internal fun AboutModule() { @@ -47,7 +45,7 @@ internal fun AboutModule() { if (!module.readme.isNullOrBlank()) { ButtonItem( onClick = { - navigator.navigate(ViewDescriptionScreenDestination(module.readme)) + navigator.navigate(DescriptionScreenDestination(module.readme)) }, ) { Icon( @@ -93,14 +91,14 @@ internal fun AboutModule() { items(it.size) { category -> AssistChip( onClick = { - navigator.navigate( - TypedModulesScreenDestination( - type = ModulesFilter.CATEGORY, - title = it[category], - query = it[category], - repo = repo, - ), - ) +// navigator.navigate( +// TypedModulesScreenDestination( +// type = ModulesFilter.CATEGORY, +// title = it[category], +// query = it[category], +// repo = repo, +// ), +// ) }, label = { Text(it[category]) }, ) diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Alerts.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Alerts.kt index d726e38ee..892c889d9 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Alerts.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Alerts.kt @@ -5,20 +5,15 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.model.online.hasBlacklist import com.dergoogler.mmrl.model.online.hasValidMessage import com.dergoogler.mmrl.ui.component.Alert import com.dergoogler.mmrl.ui.providable.LocalOnlineModule import com.dergoogler.mmrl.ui.screens.moduleView.providable.LocalModuleViewModel -import com.dergoogler.mmrl.ui.screens.settings.blacklist.items.BlacklistBottomSheet private val alertPadding = PaddingValues(horizontal = 16.dp) @@ -32,25 +27,6 @@ internal fun Alerts() { module.manager(viewModel.platform) } - module.hasBlacklist { - var open by remember { mutableStateOf(false) } - if (open) { - BlacklistBottomSheet( - module = it, - onClose = { open = false }, - ) - } - - Alert( - icon = R.drawable.alert_circle_filled, - title = stringResource(R.string.blacklisted), - backgroundColor = MaterialTheme.colorScheme.errorContainer, - textColor = MaterialTheme.colorScheme.onErrorContainer, - message = stringResource(R.string.blacklisted_desc), - outsideContentPadding = alertPadding, - ) - } - manager.isNotSupportedRootVersion(viewModel.versionCode) { min -> if (min == -1) { Alert( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Header.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Header.kt index f4739fe84..453b6bdb3 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Header.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Header.kt @@ -1,6 +1,5 @@ package com.dergoogler.mmrl.ui.screens.moduleView.sections -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -16,7 +15,6 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -24,19 +22,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.dergoogler.mmrl.R import com.dergoogler.mmrl.ext.isNotNullOrBlank -import com.dergoogler.mmrl.ext.nullable import com.dergoogler.mmrl.model.local.State import com.dergoogler.mmrl.model.online.isBlacklisted import com.dergoogler.mmrl.platform.content.LocalModule.Companion.isEmpty import com.dergoogler.mmrl.platform.content.isValid import com.dergoogler.mmrl.ui.component.Logo -import com.dergoogler.mmrl.ui.component.text.IconText import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalModule import com.dergoogler.mmrl.ui.providable.LocalOnlineModule @@ -44,8 +41,10 @@ import com.dergoogler.mmrl.ui.providable.LocalRepo import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.ui.providable.LocalVersionItem import com.dergoogler.mmrl.ui.screens.moduleView.providable.LocalModuleViewModel -import com.dergoogler.mmrl.ui.screens.repository.modules.ModulesFilter -import com.ramcosta.composedestinations.generated.destinations.TypedModulesScreenDestination +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.text.FormatText +import dev.mmrlx.compose.ui.theme.MMRLXTheme @Composable internal fun Header() { @@ -91,32 +90,39 @@ internal fun Header() { Column( modifier = Modifier.weight(1f), ) { - IconText( - style = MaterialTheme.typography.titleLarge, - tint = MaterialTheme.colorScheme.surfaceTint, - alignment = Alignment.End, + FormatText( + style = MMRLXTheme.typography.titleMedium, + text = module.name + if (module.isVerified) " %y" else "", maxLines = 2, overflow = TextOverflow.Ellipsis, - text = module.name, - resId = module.isVerified nullable R.drawable.rosette_discount_check, - ) + ) { + if (module.isVerified) { + composable { + Icon( + tint = MaterialTheme.colorScheme.surfaceTint, + modifier = Modifier.size(fontSize.dp), + painter = painterResource(id = R.drawable.rosette_discount_check), + ) + } + } + } Spacer(modifier = Modifier.height(4.dp)) Text( - modifier = - Modifier.clickable( - onClick = { - navigator.navigate( - TypedModulesScreenDestination( - type = ModulesFilter.AUTHOR, - title = module.author, - query = module.author, - repo = repo, - ), - ) - }, - ), +// modifier = +// Modifier.clickable( +// onClick = { +// navigator.navigate( +// TypedModulesScreenDestination( +// type = ModulesFilter.AUTHOR, +// title = module.author, +// query = module.author, +// repo = repo, +// ), +// ) +// }, +// ), text = module.author, style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.surfaceTint), maxLines = 2, @@ -147,7 +153,7 @@ internal fun Header() { OutlinedButton( enabled = viewModel.isProviderAlive && - (!userPreferences.useShellForModuleStateChange || it.state != com.dergoogler.mmrl.model.local.State.REMOVE), + (!userPreferences.useShellForModuleStateChange || it.state != com.dergoogler.mmrl.model.local.State.REMOVE), modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information.kt index 94ef5eda7..2c8fa0fb1 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -22,11 +21,6 @@ import com.dergoogler.mmrl.ext.takeTrue import com.dergoogler.mmrl.model.online.isValid import com.dergoogler.mmrl.platform.content.isValid import com.dergoogler.mmrl.platform.file.SuFile.Companion.toFormattedFileSize -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListItemSlot -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.CollapseItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalModule import com.dergoogler.mmrl.ui.providable.LocalOnlineModule import com.dergoogler.mmrl.ui.providable.LocalUserPreferences @@ -34,6 +28,12 @@ import com.dergoogler.mmrl.ui.providable.LocalVersionItem import com.dergoogler.mmrl.ui.screens.moduleView.items.LicenseItem import com.dergoogler.mmrl.ui.screens.moduleView.providable.LocalModuleViewModel import com.dergoogler.mmrl.utils.toFormattedDateSafely +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.list.List +import dev.mmrlx.compose.ui.list.ListItemSlot +import dev.mmrlx.compose.ui.list.component.CollapseItem +import dev.mmrlx.compose.ui.list.component.item.Icon +import dev.mmrlx.compose.ui.list.component.item.Title @Composable internal fun Information() { @@ -162,7 +162,7 @@ internal fun Information() { ), ) { CollapseItem( - meta = { icon, rotation -> + first = { icon, rotation -> Title( id = R.string.module_installed, styleTransform = { diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information0.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information0.kt index 27b61f48e..54d7a3bff 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information0.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Information0.kt @@ -12,21 +12,21 @@ import com.dergoogler.mmrl.ext.ifNotNullOrBlank import com.dergoogler.mmrl.ui.component.AntiFeaturesItem import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.PermissionItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListItemSlot -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.CollapseItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalOnlineModule import com.dergoogler.mmrl.ui.screens.moduleView.items.OtherSourcesItem import com.dergoogler.mmrl.ui.screens.moduleView.listItemContentPaddingValues import com.dergoogler.mmrl.ui.screens.moduleView.providable.LocalModuleViewModel import com.dergoogler.mmrl.ui.screens.moduleView.providable.LocalRequireModules import com.dergoogler.mmrl.ui.screens.moduleView.subListItemContentPaddingValues +import dev.mmrlx.compose.ui.list.List +import dev.mmrlx.compose.ui.list.ListItemSlot +import dev.mmrlx.compose.ui.list.component.ButtonItem +import dev.mmrlx.compose.ui.list.component.CollapseItem +import dev.mmrlx.compose.ui.list.component.Item +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Icon +import dev.mmrlx.compose.ui.list.component.item.Supporting +import dev.mmrlx.compose.ui.list.component.item.Title @Composable internal fun Information0() { @@ -39,7 +39,7 @@ internal fun Information0() { contentPadding = listItemContentPaddingValues, ) { CollapseItem( - meta = { icon, rotation -> + first = { icon, rotation -> Title(R.string.view_module_module_support) Icon( slot = ListItemSlot.End, @@ -50,57 +50,57 @@ internal fun Information0() { ) }, ) { - module.donate.ifNotNullOrBlank { - ButtonItem( - contentPadding = subListItemContentPaddingValues, - onClick = { - browser.openUri(it) - }, - ) { - Icon(painter = painterResource(id = R.drawable.currency_dollar)) - Title(R.string.view_module_donate) - Description(R.string.view_module_donate_desc) - } - } - - ButtonItem( + List( contentPadding = subListItemContentPaddingValues, - onClick = { - browser.openUri(module.track.source) - }, ) { - Icon(painter = painterResource(id = R.drawable.brand_git)) - Title(R.string.view_module_source) - } + module.donate.ifNotNullOrBlank { + ButtonItem( + onClick = { + browser.openUri(it) + }, + ) { + Icon(painter = painterResource(id = R.drawable.currency_dollar)) + Title(R.string.view_module_donate) + Description(R.string.view_module_donate_desc) + } + } - module.homepage.ifNotNullOrBlank { ButtonItem( - contentPadding = subListItemContentPaddingValues, onClick = { - browser.openUri(it) + browser.openUri(module.track.source) }, ) { - Icon(painter = painterResource(id = R.drawable.world_www)) - Title(R.string.view_module_homepage) + Icon(painter = painterResource(id = R.drawable.brand_git)) + Title(R.string.view_module_source) } - } - module.support?.ifNotNullOrBlank { - ButtonItem( - contentPadding = subListItemContentPaddingValues, - onClick = { - browser.openUri(it) - }, - ) { - Icon(painter = painterResource(id = R.drawable.heart_handshake)) - Title(R.string.view_module_support) + module.homepage.ifNotNullOrBlank { + ButtonItem( + onClick = { + browser.openUri(it) + }, + ) { + Icon(painter = painterResource(id = R.drawable.world_www)) + Title(R.string.view_module_homepage) + } + } + + module.support?.ifNotNullOrBlank { + ButtonItem( + onClick = { + browser.openUri(it) + }, + ) { + Icon(painter = painterResource(id = R.drawable.heart_handshake)) + Title(R.string.view_module_support) + } } } } module.permissions.ifNotEmpty { CollapseItem( - meta = { icon, rotation -> + first = { icon, rotation -> Title(R.string.view_module_permissions) Icon( slot = ListItemSlot.End, @@ -109,7 +109,7 @@ internal fun Information0() { .graphicsLayer(rotationZ = rotation), painter = painterResource(id = icon), ) - Labels { + Supporting { LabelItem( text = stringResource( @@ -120,16 +120,20 @@ internal fun Information0() { } }, ) { - PermissionItem( + List( contentPadding = subListItemContentPaddingValues, - permissions = it, - ) + ) { + PermissionItem( + contentPadding = subListItemContentPaddingValues, + permissions = it, + ) + } } } module.track.antifeatures.ifNotEmpty { CollapseItem( - meta = { icon, rotation -> + first = { icon, rotation -> Title(R.string.view_module_antifeatures) Icon( slot = ListItemSlot.End, @@ -138,7 +142,7 @@ internal fun Information0() { .graphicsLayer(rotationZ = rotation), painter = painterResource(id = icon), ) - Labels { + Supporting { LabelItem( text = stringResource( @@ -149,16 +153,20 @@ internal fun Information0() { } }, ) { - AntiFeaturesItem( + List( contentPadding = subListItemContentPaddingValues, - antifeatures = it, - ) + ) { + AntiFeaturesItem( + contentPadding = subListItemContentPaddingValues, + antifeatures = it, + ) + } } } requires.ifNotEmpty { requiredIds -> CollapseItem( - meta = { icon, rotation -> + first = { icon, rotation -> Title(R.string.view_module_dependencies) Icon( slot = ListItemSlot.End, @@ -167,7 +175,7 @@ internal fun Information0() { .graphicsLayer(rotationZ = rotation), painter = painterResource(id = icon), ) - Labels { + Supporting { LabelItem( text = stringResource( @@ -178,13 +186,16 @@ internal fun Information0() { } }, ) { - requiredIds.forEach { onlineModule -> - ButtonItem( - contentPadding = subListItemContentPaddingValues, - onClick = {}, - ) { - Title(onlineModule.name) - Description(onlineModule.versionCode.toString()) + List( + contentPadding = subListItemContentPaddingValues, + ) { + requiredIds.forEach { onlineModule -> + ButtonItem( + onClick = {}, + ) { + Title(onlineModule.name) + Description(onlineModule.versionCode.toString()) + } } } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Toolbar.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Toolbar0.kt similarity index 88% rename from app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Toolbar.kt rename to app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Toolbar0.kt index 92dbf8484..17aa0ee6e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Toolbar.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/moduleView/sections/Toolbar0.kt @@ -4,18 +4,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler @@ -28,7 +19,6 @@ import com.dergoogler.mmrl.ext.shareText import com.dergoogler.mmrl.model.local.BulkModule import com.dergoogler.mmrl.model.online.isValid import com.dergoogler.mmrl.platform.content.isValid -import com.dergoogler.mmrl.ui.component.toolbar.BlurToolbar import com.dergoogler.mmrl.ui.providable.LocalBulkInstall import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalModule @@ -41,11 +31,14 @@ import com.dergoogler.mmrl.ui.providable.LocalVersionItem import com.dergoogler.mmrl.ui.screens.moduleView.items.VersionsItem import com.dergoogler.mmrl.ui.screens.moduleView.providable.LocalModuleViewModel import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton import kotlinx.coroutines.launch @OptIn(ExperimentalHazeMaterialsApi::class) @Composable -internal fun Toolbar() { +internal fun Toolbar0() { val context = LocalContext.current val viewModel = LocalModuleViewModel.current val repo = LocalRepo.current @@ -61,24 +54,7 @@ internal fun Toolbar() { val scope = rememberCoroutineScope() - val state = scrollBehavior.state - - var alpha by remember { mutableFloatStateOf(0f) } - val fadeDistance = 200f - - LaunchedEffect(state) { - snapshotFlow { state.contentOffset } - .collect { offset -> - val newAlpha = ((-offset) / fadeDistance).coerceIn(0f, 1f) - - // Only update if it actually changes meaningfully - if ((newAlpha < 1f && newAlpha > 0f) || newAlpha != alpha) { - alpha = newAlpha - } - } - } - - BlurToolbar( + dev.mmrlx.compose.ui.toolbar.Toolbar( title = {}, navigationIcon = { IconButton(onClick = { navigator.popBackStack() }) { @@ -88,8 +64,6 @@ internal fun Toolbar() { ) } }, - fade = true, - fadeBackgroundIfNoBlur = true, scrollBehavior = scrollBehavior, actions = { VersionsItem( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModuleItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModuleItem.kt index 51251cc81..fbc372d0e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModuleItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModuleItem.kt @@ -1,5 +1,11 @@ package com.dergoogler.mmrl.ui.screens.modules +import android.content.ComponentName +import android.content.Intent +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DrawableRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image @@ -71,7 +77,6 @@ import com.dergoogler.mmrl.ui.component.text.BBCodeText import com.dergoogler.mmrl.ui.providable.LocalModule import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.utils.toFormattedDateSafely -import com.dergoogler.mmrl.utils.webUILauncher import dev.dergoogler.mmrl.compat.core.LocalUriHandler import kotlinx.coroutines.launch @@ -82,17 +87,17 @@ fun ModuleItem( alpha: Float = 1f, decoration: TextDecoration = TextDecoration.None, switch: - @Composable() - (() -> Unit?)? = null, + @Composable() + (() -> Unit?)? = null, indicator: - @Composable() - (CardScope.() -> Unit?)? = null, + @Composable() + (CardScope.() -> Unit?)? = null, startTrailingButton: - @Composable() - (LiteRowScope.() -> Unit)? = null, + @Composable() + (LiteRowScope.() -> Unit)? = null, trailingButton: - @Composable() - (LiteRowScope.() -> Unit), + @Composable() + (LiteRowScope.() -> Unit), isBlacklisted: Boolean = false, isProviderAlive: Boolean, ) { @@ -115,7 +120,36 @@ fun ModuleItem( !context.isPackageInstalled(userPreferences.webuixPackageName) } - val launch = userPreferences.webUILauncher(context, module) + val webui = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions(), + ) { result -> + if (!result.allGranted { perm -> + Toast + .makeText(context, "Permission denied! Requires $perm", Toast.LENGTH_SHORT) + .show() + } + ) { + return@rememberLauncherForActivityResult + } + + try { + val intent = Intent().apply { + component = ComponentName( + userPreferences.webuixPackageName, + "com.dergoogler.mmrl.wx.ui.webui.WebUIActivity" + ) + + putExtra("MODULE_ID", module.id.toString()) + } + + context.startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + Log.e("webUILauncher", "Failed to launch WebUI", e) + } + } + val clicker: (() -> Unit)? = remember(canWenUIAccessed) { @@ -125,7 +159,11 @@ fun ModuleItem( return@jump } - launch() + webui.launch( + arrayOf( + "com.dergoogler.mmrl.permission.WEBUI_X", + ), + ) } } @@ -138,10 +176,10 @@ fun ModuleItem( Card( border = isBlacklisted nullable - BorderStroke( - width = 1.dp, - color = MaterialTheme.colorScheme.errorContainer, - ), + BorderStroke( + width = 1.dp, + color = MaterialTheme.colorScheme.errorContainer, + ), onClick = clicker, ) { indicator.nullable { @@ -439,3 +477,13 @@ fun StateIndicator( alpha = 0.1f, colorFilter = ColorFilter.tint(color), ) + +private fun Map.allGranted(onDenied: (String) -> Unit): Boolean { + for ((perm, granted) in this) { + if (!granted) { + onDenied(perm) + return false + } + } + return true +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesList.kt index b7d998cae..cfea5905e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesList.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesList.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -22,7 +21,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -40,18 +38,14 @@ import com.dergoogler.mmrl.model.online.VersionItem import com.dergoogler.mmrl.platform.content.LocalModule.Companion.hasAction import com.dergoogler.mmrl.ui.activity.terminal.action.ActionActivity import com.dergoogler.mmrl.ui.component.VersionItemBottomSheet -import com.dergoogler.mmrl.ui.component.scaffold.ScaffoldScope -import com.dergoogler.mmrl.ui.component.scrollbar.VerticalFastScrollbar -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings import com.dergoogler.mmrl.ui.providable.LocalModule import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.viewmodel.ModulesViewModel -import dev.chrisbanes.haze.hazeSource +import dev.mmrlx.compose.ui.ext.with +import dev.mmrlx.compose.ui.scaffold.ScaffoldScope @Composable fun ScaffoldScope.ModulesList( - innerPadding: PaddingValues, list: List, state: LazyListState, onDownload: (LocalModule, VersionItem, Boolean) -> Unit, @@ -60,53 +54,40 @@ fun ScaffoldScope.ModulesList( ) = Box( modifier = Modifier.fillMaxSize(), ) { - val paddingValues = LocalMainScreenInnerPaddings.current val layoutDirection = LocalLayoutDirection.current - this@ModulesList.ResponsiveContent { - LazyColumn( - state = state, - modifier = - Modifier - .fillMaxSize() - .hazeSource(state = LocalHazeState.current), - contentPadding = - PaddingValues( - top = innerPadding.calculateTopPadding() + 16.dp, - start = innerPadding.calculateStartPadding(layoutDirection) + 16.dp, - bottom = paddingValues.calculateBottomPadding() + 16.dp, - end = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - items( - items = list, - key = { it.id.id }, - contentType = { "module_item" }, - ) { module -> - CompositionLocalProvider( - LocalModule provides module, - ) { - ModuleItem( - viewModel = viewModel, - onDownload = onDownload, - isProviderAlive = isProviderAlive, - ) - } - } - } - } - - VerticalFastScrollbar( + LazyColumn( state = state, modifier = Modifier - .align(Alignment.CenterEnd) - .padding( - top = innerPadding.calculateTopPadding(), - bottom = paddingValues.calculateBottomPadding(), - ), - ) + .with(this@ModulesList) { + it.scaffoldHazeSource() + }, + contentPadding = + PaddingValues( + top = this@ModulesList.scaffoldTopPadding + 16.dp, + start = this@ModulesList.contentPadding.calculateStartPadding(layoutDirection) + 16.dp, + bottom = this@ModulesList.contentPadding.calculateBottomPadding() + 16.dp, + end = 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items( + items = list, + key = { it.id.id }, + contentType = { "module_item" }, + ) { module -> + CompositionLocalProvider( + LocalModule provides module, + ) { + ModuleItem( + viewModel = viewModel, + onDownload = onDownload, + isProviderAlive = isProviderAlive, + ) + } + } + } } @Composable @@ -230,10 +211,10 @@ private fun ModuleItem( isProviderAlive, ) { isProviderAlive && - ( - !(viewModel.moduleCompatibility.canRestoreModules && userPreferences.useShellForModuleStateChange) || - module.state != State.REMOVE - ) + ( + !(viewModel.moduleCompatibility.canRestoreModules && userPreferences.useShellForModuleStateChange) || + module.state != State.REMOVE + ) } RemoveOrRestore( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesMenu.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesMenu.kt index a2e8c6fd7..78eeae984 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesMenu.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesMenu.kt @@ -8,11 +8,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -38,6 +35,9 @@ import com.dergoogler.mmrl.ui.component.Segment import com.dergoogler.mmrl.ui.component.SegmentedButtons import com.dergoogler.mmrl.ui.component.SegmentedButtonsDefaults import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton @Composable fun ModulesMenu(setMenu: (ModulesMenu) -> Unit) { diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesScreen.kt index f2957e11f..b3c9c66b7 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/modules/ModulesScreen.kt @@ -9,14 +9,11 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -38,96 +35,106 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.dergoogler.mmrl.R import com.dergoogler.mmrl.datastore.model.ModulesMenu -import com.dergoogler.mmrl.ext.currentScreenWidth import com.dergoogler.mmrl.ext.isScrollingUp import com.dergoogler.mmrl.ext.none import com.dergoogler.mmrl.ext.systemBarsPaddingEnd import com.dergoogler.mmrl.model.local.LocalModule import com.dergoogler.mmrl.model.online.VersionItem import com.dergoogler.mmrl.ui.activity.terminal.install.InstallActivity +import com.dergoogler.mmrl.ui.component.BottomBar import com.dergoogler.mmrl.ui.component.Loading -import com.dergoogler.mmrl.ui.component.LocalScreenProvider import com.dergoogler.mmrl.ui.component.PageIndicator import com.dergoogler.mmrl.ui.component.TopAppBarEventIcon -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold import com.dergoogler.mmrl.ui.component.toolbar.BlurSearchToolbar import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.viewmodel.ModulesViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton +import dev.mmrlx.compose.ui.scaffold.Scaffold +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior @Destination @Composable -fun ModulesScreen(viewModel: ModulesViewModel = hiltViewModel()) = - LocalScreenProvider { - val userPrefs = LocalUserPreferences.current - val context = LocalContext.current +fun ModulesScreen(viewModel: ModulesViewModel = hiltViewModel()) { + val userPrefs = LocalUserPreferences.current + val context = LocalContext.current - val list by viewModel.local.collectAsStateWithLifecycle() - val query by viewModel.query.collectAsStateWithLifecycle() - val state by viewModel.screenState.collectAsStateWithLifecycle() - val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() + val list by viewModel.local.collectAsStateWithLifecycle() + val query by viewModel.query.collectAsStateWithLifecycle() + val state by viewModel.screenState.collectAsStateWithLifecycle() + val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = ToolbarDefaults.fadePinnedScrollBehavior() - val pullToRefreshState = rememberPullToRefreshState() + val pullToRefreshState = rememberPullToRefreshState() - val isScrollingUp by viewModel.listState.isScrollingUp() - val showFab by remember { - derivedStateOf { - isScrollingUp && !viewModel.isSearch && viewModel.isProviderAlive - } + val isScrollingUp by viewModel.listState.isScrollingUp() + val showFab by remember { + derivedStateOf { + isScrollingUp && !viewModel.isSearch && viewModel.isProviderAlive } + } - val download: (LocalModule, VersionItem, Boolean) -> Unit = { module, item, install -> - viewModel.downloader(context, module, item) { - if (install) { - InstallActivity.start( - context = context, - uri = it.toUri(), - ) - } + val download: (LocalModule, VersionItem, Boolean) -> Unit = { module, item, install -> + viewModel.downloader(context, module, item) { + if (install) { + InstallActivity.start( + context = context, + uri = it.toUri(), + ) } } + } - BackHandler( - enabled = viewModel.isSearch, - onBack = viewModel::closeSearch, - ) + BackHandler( + enabled = viewModel.isSearch, + onBack = viewModel::closeSearch, + ) - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopBar( - isSearch = viewModel.isSearch, - query = query, - onQueryChange = viewModel::search, - onOpenSearch = viewModel::openSearch, - onCloseSearch = viewModel::closeSearch, - setMenu = viewModel::setModulesMenu, - scrollBehavior = scrollBehavior, - ) - }, - floatingActionButton = { - AnimatedVisibility( - visible = showFab, - enter = - scaleIn( - animationSpec = tween(100), - initialScale = 0.8f, - ), - exit = - scaleOut( - animationSpec = tween(100), - targetScale = 0.8f, - ), - ) { - FloatingButton() - } - }, - contentWindowInsets = WindowInsets.none, - ) { innerPadding -> + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + toolbar = { + TopBar0( + isSearch = viewModel.isSearch, + query = query, + onQueryChange = viewModel::search, + onOpenSearch = viewModel::openSearch, + onCloseSearch = viewModel::closeSearch, + setMenu = viewModel::setModulesMenu, + scrollBehavior = scrollBehavior, + ) + }, + bottomBar = { + BottomBar() + }, + floatingActionButton = { + AnimatedVisibility( + visible = showFab, + enter = + scaleIn( + animationSpec = tween(100), + initialScale = 0.8f, + ), + exit = + scaleOut( + animationSpec = tween(100), + targetScale = 0.8f, + ), + ) { + FloatingButton() + } + }, + contentWindowInsets = WindowInsets.none, + ) { + Box( + modifier = + Modifier + .scaffoldHazeSource() + ) { if (isLoading) { Loading() } @@ -146,20 +153,13 @@ fun ModulesScreen(viewModel: ModulesViewModel = hiltViewModel()) = indicator = { Indicator( modifier = - Modifier.align(Alignment.TopCenter).let { - if (!userPrefs.enableBlur) { - it.padding(top = innerPadding.calculateTopPadding()) - } else { - it - } - }, + Modifier.align(Alignment.TopCenter), isRefreshing = state.isRefreshing, state = pullToRefreshState, ) }, ) { this@Scaffold.ModulesList( - innerPadding = innerPadding, list = list, state = viewModel.listState, viewModel = viewModel, @@ -169,30 +169,26 @@ fun ModulesScreen(viewModel: ModulesViewModel = hiltViewModel()) = } } } +} @Composable -private fun TopBar( +private fun TopBar0( isSearch: Boolean, query: String, onQueryChange: (String) -> Unit, onOpenSearch: () -> Unit, onCloseSearch: () -> Unit, setMenu: (ModulesMenu) -> Unit, - scrollBehavior: TopAppBarScrollBehavior, + scrollBehavior: ToolbarScrollBehavior, ) { - val width = currentScreenWidth() - var currentQuery by remember { mutableStateOf(query) } DisposableEffect(isSearch) { onDispose { currentQuery = "" } } BlurSearchToolbar( - scrollBehavior = scrollBehavior, isSearch = isSearch, query = currentQuery, - fade = true, - fadeDistance = 50f, onQueryChange = { onQueryChange(it) currentQuery = it @@ -202,8 +198,6 @@ private fun TopBar( currentQuery = "" }, title = { - if (!width.isSmall) return@BlurSearchToolbar - TopAppBarEventIcon() }, actions = { @@ -222,6 +216,7 @@ private fun TopBar( setMenu = setMenu, ) }, + scrollBehavior = scrollBehavior, ) } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesList.kt index c224ec578..2b2aa5ec8 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesList.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesList.kt @@ -2,12 +2,7 @@ package com.dergoogler.mmrl.ui.screens.repositories import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -23,21 +18,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R +import com.dergoogler.mmrl.ext.plus import com.dergoogler.mmrl.model.state.RepoState -import com.dergoogler.mmrl.ui.component.scaffold.ScaffoldScope -import com.dergoogler.mmrl.ui.component.scrollbar.VerticalFastScrollbar import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings import com.dergoogler.mmrl.ui.screens.repositories.items.ExploreReposCard import com.ramcosta.composedestinations.generated.destinations.RepositoryScreenDestination -import dev.chrisbanes.haze.hazeSource +import dev.mmrlx.compose.ui.scaffold.ScaffoldScope @Composable fun ScaffoldScope.RepositoriesList( @@ -45,64 +35,34 @@ fun ScaffoldScope.RepositoriesList( state: LazyListState, delete: (RepoState) -> Unit, getUpdate: (RepoState, (Throwable) -> Unit) -> Unit, - innerPadding: PaddingValues, ) = Box( modifier = Modifier.fillMaxSize(), ) { - val paddingValues = LocalMainScreenInnerPaddings.current - val layoutDirection = LocalLayoutDirection.current val navigator = LocalDestinationsNavigator.current - this@RepositoriesList.ResponsiveContent { - LazyColumn( - state = state, - modifier = - Modifier - .fillMaxSize() - .hazeSource(state = LocalHazeState.current), - contentPadding = - PaddingValues( - top = innerPadding.calculateTopPadding() + 16.dp, - start = innerPadding.calculateStartPadding(layoutDirection) + 16.dp, - bottom = 16.dp, - end = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - item { - ExploreReposCard() - } - - items( - items = list, - key = { it.url }, - ) { repo -> - RepositoryItem( - repo = repo, - onClick = { - navigator.navigate(RepositoryScreenDestination(repo.toRepo())) - }, - onUpdate = getUpdate, - onDelete = delete, - ) - } + LazyColumn( + state = state, + contentPadding = this@RepositoriesList.contentPadding + 16.dp, + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + item { + ExploreReposCard() + } - item { - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) - } + items( + items = list, + key = { it.url }, + ) { repo -> + RepositoryItem( + repo = repo, + onClick = { + navigator.navigate(RepositoryScreenDestination(repo.toRepo())) + }, + onUpdate = getUpdate, + onDelete = delete, + ) } } - - VerticalFastScrollbar( - state = state, - modifier = - Modifier - .align(Alignment.CenterEnd) - .padding( - top = innerPadding.calculateTopPadding(), - bottom = paddingValues.calculateBottomPadding(), - ), - ) } @Composable diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesMenu.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesMenu.kt index 4b736cd89..b26767330 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesMenu.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesMenu.kt @@ -6,10 +6,7 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -29,6 +26,9 @@ import com.dergoogler.mmrl.ui.component.Segment import com.dergoogler.mmrl.ui.component.SegmentedButtons import com.dergoogler.mmrl.ui.component.SegmentedButtonsDefaults import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton @Composable fun RepositoriesMenu(setMenu: (RepositoriesMenu) -> Unit) { diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesScreen.kt index a9da830cd..b7416dbad 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repositories/RepositoriesScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -11,15 +12,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -43,7 +39,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.dergoogler.mmrl.R import com.dergoogler.mmrl.datastore.model.RepositoriesMenu -import com.dergoogler.mmrl.ext.currentScreenWidth import com.dergoogler.mmrl.ext.isScrollingUp import com.dergoogler.mmrl.ext.none import com.dergoogler.mmrl.ext.rememberSaveableLazyListState @@ -52,13 +47,11 @@ import com.dergoogler.mmrl.model.local.BulkModule import com.dergoogler.mmrl.ui.activity.terminal.install.InstallActivity import com.dergoogler.mmrl.ui.animate.slideInTopToBottom import com.dergoogler.mmrl.ui.animate.slideOutBottomToTop +import com.dergoogler.mmrl.ui.component.BottomBar import com.dergoogler.mmrl.ui.component.Loading -import com.dergoogler.mmrl.ui.component.LocalScreenProvider import com.dergoogler.mmrl.ui.component.PageIndicator import com.dergoogler.mmrl.ui.component.TopAppBarEventIcon import com.dergoogler.mmrl.ui.component.dialog.TextFieldDialog -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.component.toolbar.BlurToolbar import com.dergoogler.mmrl.ui.providable.LocalBulkInstall import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings @@ -68,104 +61,114 @@ import com.dergoogler.mmrl.viewmodel.RepositoriesViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.SearchScreenDestination +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton +import dev.mmrlx.compose.ui.scaffold.Scaffold +import dev.mmrlx.compose.ui.toolbar.Toolbar +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior import timber.log.Timber import kotlin.reflect.KFunction1 @Destination @Composable -fun RepositoriesScreen() = - LocalScreenProvider { - val viewModel = hiltViewModel() +fun RepositoriesScreen() { + val viewModel = hiltViewModel() - val userPrefs = LocalUserPreferences.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - val list by viewModel.repos.collectAsStateWithLifecycle() - val bulkInstallViewModel = LocalBulkInstall.current - val bulkModules by bulkInstallViewModel.bulkModules.collectAsStateWithLifecycle() + val userPrefs = LocalUserPreferences.current + val scrollBehavior = ToolbarDefaults.fadePinnedScrollBehavior() + val list by viewModel.repos.collectAsStateWithLifecycle() + val bulkInstallViewModel = LocalBulkInstall.current + val bulkModules by bulkInstallViewModel.bulkModules.collectAsStateWithLifecycle() - val state by viewModel.screenState.collectAsStateWithLifecycle() - val progress by viewModel.progress.collectAsStateWithLifecycle() + val state by viewModel.screenState.collectAsStateWithLifecycle() + val progress by viewModel.progress.collectAsStateWithLifecycle() - val pullToRefreshState = rememberPullToRefreshState() - - // Use rememberSaveable to persist scroll position across navigation - val listState = rememberSaveableLazyListState(key = "repositories_list") + val pullToRefreshState = rememberPullToRefreshState() - val showFab by listState.isScrollingUp() + // Use rememberSaveable to persist scroll position across navigation + val listState = rememberSaveableLazyListState(key = "repositories_list") - var repoUrl by remember { mutableStateOf("") } - var message: String by remember { mutableStateOf("") } + val showFab by listState.isScrollingUp() - var failure by remember { mutableStateOf(false) } - if (failure) { - FailureDialog( - name = repoUrl, - message = message, - onClose = { - failure = false - message = "" - }, - ) - } + var repoUrl by remember { mutableStateOf("") } + var message: String by remember { mutableStateOf("") } - var add by remember { mutableStateOf(false) } - if (add) { - AddDialog( - onClose = { add = false }, - onAdd = { - repoUrl = it - viewModel.insert(it) { e -> - failure = true - message = e.stackTraceToString() - } - }, - ) - } + var failure by remember { mutableStateOf(false) } + if (failure) { + FailureDialog( + name = repoUrl, + message = message, + onClose = { + failure = false + message = "" + }, + ) + } - val context = LocalContext.current - var bulkInstallBottomSheet by remember { mutableStateOf(false) } + var add by remember { mutableStateOf(false) } + if (add) { + AddDialog( + onClose = { add = false }, + onAdd = { + repoUrl = it + viewModel.insert(it) { e -> + failure = true + message = e.stackTraceToString() + } + }, + ) + } - val bulkDownload: (List, Boolean) -> Unit = { item, install -> - bulkInstallViewModel.downloadMultiple( - items = item, - onAllSuccess = { - bulkInstallViewModel.clearBulkModules() - bulkInstallBottomSheet = false - if (install) { - InstallActivity.start( - context = context, - uri = it, - ) - } - }, - onFailure = { - Timber.e(it) - }, - ) - } + val context = LocalContext.current + var bulkInstallBottomSheet by remember { mutableStateOf(false) } - if (bulkInstallBottomSheet) { - BulkBottomSheet( - onClose = { - bulkInstallBottomSheet = false - }, - modules = bulkModules, - onDownload = bulkDownload, - bulkInstallViewModel = bulkInstallViewModel, - ) - } + val bulkDownload: (List, Boolean) -> Unit = { item, install -> + bulkInstallViewModel.downloadMultiple( + items = item, + onAllSuccess = { + bulkInstallViewModel.clearBulkModules() + bulkInstallBottomSheet = false + if (install) { + InstallActivity.start( + context = context, + uri = it, + ) + } + }, + onFailure = { + Timber.e(it) + }, + ) + } - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopBar( - setMenu = viewModel::setRepositoriesMenu, - onAdd = { add = true }, - scrollBehavior = scrollBehavior, - ) + if (bulkInstallBottomSheet) { + BulkBottomSheet( + onClose = { + bulkInstallBottomSheet = false }, - contentWindowInsets = WindowInsets.none, - floatingActionButton = { + modules = bulkModules, + onDownload = bulkDownload, + bulkInstallViewModel = bulkInstallViewModel, + ) + } + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + toolbar = { + TopBar0( + setMenu = viewModel::setRepositoriesMenu, + onAdd = { add = true }, + scrollBehavior = scrollBehavior, + ) + }, + bottomBar = { + BottomBar() + }, + contentWindowInsets = WindowInsets.none, + floatingActionButton = { + if (viewModel.platform.isValid) { AnimatedVisibility( visible = showFab, enter = @@ -185,9 +188,14 @@ fun RepositoriesScreen() = }, ) } - }, - ) { innerPadding -> - + } + }, + ) { + Box( + modifier = + Modifier + .scaffoldHazeSource() + ) { if (viewModel.isLoading) { Loading() } @@ -205,21 +213,13 @@ fun RepositoriesScreen() = onRefresh = viewModel::getRepoAll, indicator = { Indicator( - modifier = - Modifier.align(Alignment.TopCenter).let { - if (!userPrefs.enableBlur) { - it.padding(top = innerPadding.calculateTopPadding()) - } else { - it - } - }, + modifier = Modifier.align(Alignment.TopCenter), isRefreshing = state.isRefreshing, state = pullToRefreshState, ) }, ) { this@Scaffold.RepositoriesList( - innerPadding = innerPadding, list = list, state = listState, delete = viewModel::delete, @@ -233,19 +233,13 @@ fun RepositoriesScreen() = exit = slideOutBottomToTop(), ) { LinearProgressIndicator( - modifier = - Modifier.fillMaxWidth().let { - if (!userPrefs.enableBlur) { - it.padding(top = innerPadding.calculateTopPadding()) - } else { - it - } - }, + modifier = Modifier.fillMaxWidth(), strokeCap = StrokeCap.Round, ) } } } +} @Composable private fun AddDialog( @@ -300,23 +294,18 @@ private fun AddDialog( } @Composable -private fun TopBar( +private fun TopBar0( onAdd: () -> Unit, setMenu: KFunction1, - scrollBehavior: TopAppBarScrollBehavior, + scrollBehavior: ToolbarScrollBehavior, ) { - val width = currentScreenWidth() val navigator = LocalDestinationsNavigator.current - BlurToolbar( + Toolbar( scrollBehavior = scrollBehavior, title = { - if (!width.isSmall) return@BlurToolbar - TopAppBarEventIcon() }, - fadeDistance = 50f, - fade = true, actions = { IconButton( onClick = { diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModulesList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModulesList.kt new file mode 100644 index 000000000..4849f60ac --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/ModulesList.kt @@ -0,0 +1,85 @@ +package com.dergoogler.mmrl.ui.screens.repository + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.datastore.model.RepoListMode +import com.dergoogler.mmrl.model.online.OnlineModule +import com.dergoogler.mmrl.model.state.OnlineState +import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator +import com.dergoogler.mmrl.ui.providable.LocalOnlineModule +import com.dergoogler.mmrl.ui.providable.LocalOnlineModuleState +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import com.ramcosta.composedestinations.generated.destinations.NewViewScreenDestination +import dev.mmrlx.compose.ui.scaffold.ScaffoldScope + +@Composable +fun ScaffoldScope.ModulesList( + repo: Repo, + list: List>, + state: LazyListState, + beforeContent: LazyListScope.() -> Unit = {}, +) { + val navigator = LocalDestinationsNavigator.current + val userPreferences = LocalUserPreferences.current + val menu = userPreferences.repositoryMenu + + Box( + modifier = Modifier.fillMaxSize(), + ) { + LazyColumn( + state = state, + modifier = Modifier + .animateContentSize(), + contentPadding = + PaddingValues( + bottom = this@ModulesList.scaffoldBottomPadding + 16.dp, + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + beforeContent() + + items( + items = list, + key = { it.second.id }, + ) { (moduleState, module) -> + CompositionLocalProvider( + LocalOnlineModuleState provides moduleState, + LocalOnlineModule provides module, + ) { + val click = fun() { + navigator.navigate( + NewViewScreenDestination(repo, module), + ) + } + + Box(Modifier.padding(horizontal = 16.dp)) { + when (menu.repoListMode) { + RepoListMode.Compact -> + ModuleItemCompact( + onClick = click, + ) + + RepoListMode.Detailed -> + ModuleItemDetailed( + onClick = click, + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryMenu.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryMenu.kt index 853d31547..b6376bc9a 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryMenu.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryMenu.kt @@ -6,10 +6,7 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,6 +27,9 @@ import com.dergoogler.mmrl.ui.component.Segment import com.dergoogler.mmrl.ui.component.SegmentedButtons import com.dergoogler.mmrl.ui.component.SegmentedButtonsDefaults import com.dergoogler.mmrl.ui.providable.LocalUserPreferences +import dev.mmrlx.compose.ui.Text +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton @Composable fun RepositoryMenu(setMenu: (RepositoryMenu) -> Unit) { @@ -190,7 +190,7 @@ private fun MenuBottomSheet( MenuChip( selected = menu.showCategory, - onClick = { setMenu(menu.copy(showIcon = !menu.showCategory)) }, + onClick = { setMenu(menu.copy(showCategory = !menu.showCategory)) }, label = { Text(text = stringResource(id = R.string.menu_show_category)) }, ) diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryScreen.kt index 4903fa61a..e66ca0773 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/RepositoryScreen.kt @@ -1,367 +1,285 @@ package com.dergoogler.mmrl.ui.screens.repository import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.dergoogler.mmrl.R import com.dergoogler.mmrl.database.entity.Repo +import com.dergoogler.mmrl.datastore.model.RepositoryMenu import com.dergoogler.mmrl.ext.fadingEdge -import com.dergoogler.mmrl.ext.isNotNullOrBlank import com.dergoogler.mmrl.ext.none -import com.dergoogler.mmrl.ext.nullply -import com.dergoogler.mmrl.ext.onClick -import com.dergoogler.mmrl.ext.rememberSaveableLazyListState -import com.dergoogler.mmrl.ext.stripLinks -import com.dergoogler.mmrl.model.ui.TopCategory +import com.dergoogler.mmrl.ui.component.BottomBar import com.dergoogler.mmrl.ui.component.Cover -import com.dergoogler.mmrl.ui.component.LabelItem -import com.dergoogler.mmrl.ui.component.LabelItemDefaults -import com.dergoogler.mmrl.ui.component.Loading -import com.dergoogler.mmrl.ui.component.LocalScreenProvider import com.dergoogler.mmrl.ui.component.PageIndicator -import com.dergoogler.mmrl.ui.component.card.Card -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.component.text.BBCodeText -import com.dergoogler.mmrl.ui.component.toolbar.BlurToolbar -import com.dergoogler.mmrl.ui.component.toolbar.ToolbarTitle -import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings -import com.dergoogler.mmrl.ui.providable.LocalRepo -import com.dergoogler.mmrl.ui.screens.repository.modules.ModulesFilter +import com.dergoogler.mmrl.ui.component.toolbar.BlurSearchToolbar +import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.viewmodel.RepositoryViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.generated.destinations.TypedModulesScreenDestination -import dev.chrisbanes.haze.hazeSource import dev.dergoogler.mmrl.compat.core.LocalUriHandler +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton +import dev.mmrlx.compose.ui.scaffold.Scaffold +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior -@Destination() +@Destination @Composable -fun RepositoryScreen(repo: Repo) = - LocalScreenProvider { - val viewModel = RepositoryViewModel.build(repo) - val list by viewModel.online.collectAsStateWithLifecycle() +fun RepositoryScreen( + repo: Repo, +) { + val userPreferences = LocalUserPreferences.current + val browser = LocalUriHandler.current + val density = LocalDensity.current - val browser = LocalUriHandler.current - val density = LocalDensity.current - val navigator = LocalDestinationsNavigator.current - val hazeState = LocalHazeState.current + val viewModel = RepositoryViewModel.build(repo) + val modules by viewModel.online.collectAsStateWithLifecycle() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val query by viewModel.query.collectAsStateWithLifecycle() + val openSearch = viewModel::openSearch + val closeSearch = viewModel::closeSearch + val search = viewModel::search + val isSearch by viewModel::isSearch + val scrollBehavior = ToolbarDefaults.fadePinnedScrollBehavior() - // Use rememberSaveable to persist scroll position across navigation - val listState = rememberSaveableLazyListState(key = "repository_${repo.url}") + var coverHeight by remember { mutableIntStateOf(0) } - BackHandler( - enabled = viewModel.isSearch, - onBack = viewModel::closeSearch, - ) + BackHandler(enabled = viewModel.isSearch, onBack = closeSearch) - val modules = remember(list) { list.map { it.second } } - val topCategories = remember(modules) { TopCategory.fromModuleList(modules) } - val metaModules = remember(modules) { - modules.filter { - it.permissions?.contains("kernelsu.permission.METAMODULE") == true - } - } - - var coverHeight by remember { mutableIntStateOf(0) } - var cardHeight by remember { mutableIntStateOf(0) } - - CompositionLocalProvider( - LocalRepo provides repo, + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + toolbar = { + TopBar0( + repo = repo, + isSearch = isSearch, + query = query, + onQueryChange = search, + onOpenSearch = openSearch, + onCloseSearch = closeSearch, + scrollBehavior = scrollBehavior, + setMenu = viewModel::setRepositoryMenu, + ) + }, + bottomBar = { + BottomBar() + }, + contentWindowInsets = WindowInsets.none, + ) { + Box( + modifier = + Modifier + .scaffoldHazeSource() ) { - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - BlurToolbar( - fadeBackgroundIfNoBlur = true, - navigationIcon = { - IconButton(onClick = { navigator.popBackStack() }) { - Icon( - painter = painterResource(id = R.drawable.arrow_left), - contentDescription = null, - ) - } - }, - title = { - ToolbarTitle( - modifier = Modifier.alpha(it), - title = repo.name, - ) - }, - fade = true, - scrollBehavior = scrollBehavior, - ) - }, - contentWindowInsets = WindowInsets.none, - ) { _ -> - ResponsiveContent { - if (viewModel.isLoading) { - Loading() - - return@ResponsiveContent - } + if (modules.isEmpty()) { + PageIndicator( + icon = R.drawable.cloud, + text = if (isSearch) R.string.search_empty else R.string.repository_empty, + ) - if (list.isEmpty() && !viewModel.isLoading) { - PageIndicator( - icon = R.drawable.cloud, - text = if (viewModel.isSearch) R.string.search_empty else R.string.repository_empty, - ) - - return@ResponsiveContent - } + return@Scaffold + } - LazyColumn( - state = listState, - modifier = - Modifier - .fillMaxSize() - .hazeSource(state = hazeState), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - item { - Box { - Cover( - modifier = - Modifier - .fadingEdge( - brush = - Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Color.Black - ), - startY = Float.POSITIVE_INFINITY, - endY = 0f, + this@Scaffold.ModulesList( + repo = repo, + list = modules, + state = viewModel.listState, + ) { + item { + AnimatedVisibility(!isSearch, enter = scaleIn(), exit = scaleOut()) { + Box(Modifier.animateEnterExit(scaleIn(), scaleOut())) { + Cover( + modifier = + Modifier + .fadingEdge( + brush = + Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.Black ), - ) - .onGloballyPositioned { coordinates -> - coverHeight = coordinates.size.height - }, - url = repo.cover ?: "ERROR ME", - errorIcon = R.drawable.box, - ) - - Box( - modifier = - Modifier - .offset(y = 164.4.dp) - .align(Alignment.TopCenter) - .fillMaxWidth(0.85f) - .onGloballyPositioned { coordinates -> - cardHeight = coordinates.size.height - }, - ) { - Card( - modifier = - Modifier - .align(Alignment.BottomCenter) - .background( - brush = - Brush.linearGradient( - colors = - listOf( - MaterialTheme.colorScheme.surfaceColorAtElevation( - 1.dp, - ), - MaterialTheme.colorScheme.background, - ), - start = Offset(0f, 0f), - end = - Offset( - Float.POSITIVE_INFINITY, - Float.POSITIVE_INFINITY, - ), - ), - RoundedCornerShape(20.dp), - ) - .border( - Dp.Hairline, - MaterialTheme.colorScheme.outlineVariant, - RoundedCornerShape(20.dp), + startY = Float.POSITIVE_INFINITY, + endY = 0f, ), - ) { - Column( - modifier = - Modifier - .relative() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Text( - text = repo.name, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - ) - - BBCodeText( - text = - ( - repo.description - ?: stringResource(R.string.view_module_no_description) - ).stripLinks( - "", - ), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.outlineVariant, - lineHeight = 20.sp, - ) - - FlowRow { - repo.nullply { - if (submission.isNotNullOrBlank()) { - LabelItem( - style = - LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - ), - modifier = - Modifier.onClick { - browser.openUri(submission) - }, - icon = getDomainIcon(submission), - text = stringResource(R.string.repo_options_submission), - ) - } - } - } - } - } - } - - with(density) { - Spacer(modifier = Modifier.height(((coverHeight / 1.2f) + cardHeight).toDp())) - } - } - } - - item { - val takenModules = remember { modules.take(9) } - val title = stringResource(R.string.page_modules) - - TopPicks( - label = title, - list = takenModules, - onMoreClick = { - navigator.navigate( - TypedModulesScreenDestination( - type = ModulesFilter.ALL, - title = title, - repo = repo, - ), - ) - }, - ) - } - - if (!metaModules.isEmpty() && viewModel.platform.isKernelSuVariant) { - item { - val title = stringResource(R.string.meta_modules) - - TopPicks( - label = title, - icon = com.dergoogler.mmrl.ui.R.drawable.kernelsu_logo, - list = metaModules, - onMoreClick = { - navigator.navigate( - TypedModulesScreenDestination( - type = ModulesFilter.ALL, - title = title, - repo = repo, - ), ) - }, - ) - } - } - - items( - items = topCategories, - key = { it.label }, - ) { - TopPicks( - label = it.label, - list = it.modules, - onMoreClick = { - navigator.navigate( - TypedModulesScreenDestination( - type = ModulesFilter.CATEGORY, - title = it.label, - repo = repo, - query = it.label, - ), - ) - }, + .onGloballyPositioned { coordinates -> + coverHeight = coordinates.size.height + }, + url = repo.cover ?: "ERROR ME", + errorIcon = R.drawable.box, ) - } - - item { - Spacer( - Modifier.height(16.dp), - ) - - val paddingValues = LocalMainScreenInnerPaddings.current - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) +// +// Box( +// modifier = +// Modifier +// .offset(y = 164.4.dp) +// .align(Alignment.TopCenter) +// .fillMaxWidth(0.85f) +// .onGloballyPositioned { coordinates -> +// cardHeight = coordinates.size.height +// }, +// ) { +// Card( +// modifier = +// Modifier +// .align(Alignment.BottomCenter) +// .background( +// brush = +// Brush.linearGradient( +// colors = +// listOf( +// MaterialTheme.colorScheme.surfaceColorAtElevation( +// 1.dp, +// ), +// MaterialTheme.colorScheme.background, +// ), +// start = Offset(0f, 0f), +// end = +// Offset( +// Float.POSITIVE_INFINITY, +// Float.POSITIVE_INFINITY, +// ), +// ), +// RoundedCornerShape(20.dp), +// ) +// .border( +// Dp.Hairline, +// MaterialTheme.colorScheme.outlineVariant, +// RoundedCornerShape(20.dp), +// ), +// ) { +// Column( +// modifier = +// Modifier +// .relative() +// .padding(16.dp), +// horizontalAlignment = Alignment.CenterHorizontally, +// verticalArrangement = Arrangement.spacedBy(16.dp), +// ) { +// Text( +// text = repo.name, +// maxLines = 2, +// overflow = TextOverflow.Ellipsis, +// textAlign = TextAlign.Center, +// style = MaterialTheme.typography.headlineMedium, +// fontWeight = FontWeight.Bold, +// color = MaterialTheme.colorScheme.onBackground, +// ) +// +// BBCodeText( +// text = +// ( +// repo.description +// ?: stringResource(R.string.view_module_no_description) +// ).stripLinks( +// "", +// ), +// style = MaterialTheme.typography.bodyMedium, +// textAlign = TextAlign.Center, +// color = MaterialTheme.colorScheme.outlineVariant, +// lineHeight = 20.sp, +// ) +// +// FlowRow { +// repo.nullply { +// if (submission.isNotNullOrBlank()) { +// LabelItem( +// style = +// LabelItemDefaults.style.copy( +// containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, +// contentColor = MaterialTheme.colorScheme.onSurfaceVariant, +// ), +// modifier = +// Modifier.onClick { +// browser.openUri(submission) +// }, +// icon = getDomainIcon(submission), +// text = stringResource(R.string.repo_options_submission), +// ) +// } +// } +// } +// } +// } +// } } } } } } } +} + +@Composable +private fun TopBar0( + repo: Repo, + isSearch: Boolean, + query: String, + onQueryChange: (String) -> Unit, + onOpenSearch: () -> Unit, + onCloseSearch: () -> Unit, + scrollBehavior: ToolbarScrollBehavior, + setMenu: (RepositoryMenu) -> Unit, +) { + var currentQuery by remember { mutableStateOf(query) } + DisposableEffect(isSearch) { + onDispose { currentQuery = "" } + } + + BlurSearchToolbar( + isSearch = isSearch, + query = currentQuery, + scrollBehavior = scrollBehavior, + onQueryChange = { + onQueryChange(it) + currentQuery = it + }, + onClose = { + onCloseSearch() + currentQuery = "" + }, + title = { +// ToolbarTitle( +// title = repo.name, +// modifier = Modifier.graphicsLayer { alpha = scrollBehavior.fadeDistance }, +// ) + }, + actions = { + if (!isSearch) { + IconButton( + onClick = onOpenSearch, + ) { + Icon( + painter = painterResource(id = R.drawable.search), + contentDescription = null, + ) + } + } + + RepositoryMenu( + setMenu = setMenu, + ) + }, + ) +} @Composable fun getDomainIcon(url: String) = @@ -378,4 +296,4 @@ fun getDomain(url: String): String = if (host.startsWith("www.")) host.substring(4) else host } catch (_: Exception) { "" - } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/TopPickModule.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/TopPickModule.kt deleted file mode 100644 index 0171099bd..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/TopPickModule.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.repository - -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ext.iconSize -import com.dergoogler.mmrl.ui.component.Logo -import com.dergoogler.mmrl.ui.component.card.Card -import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalOnlineModule -import com.dergoogler.mmrl.ui.providable.LocalRepo -import com.dergoogler.mmrl.ui.providable.LocalUserPreferences -import com.ramcosta.composedestinations.generated.destinations.NewViewScreenDestination - -@Composable -fun TopPickModule( - modifier: Modifier = Modifier, - enabled: Boolean = true, -) { - val navigator = LocalDestinationsNavigator.current - val module = LocalOnlineModule.current - val repo = LocalRepo.current - val userPreferences = LocalUserPreferences.current - val menu = userPreferences.repositoryMenu - val isVerified = module.isVerified && menu.showVerified - - Card( - onClick = { - navigator.navigate(NewViewScreenDestination(repo, module)) - }, - modifier = - modifier - .fillMaxWidth() - .border( - width = Dp.Hairline, - color = MaterialTheme.colorScheme.outlineVariant, - shape = RoundedCornerShape(20.dp), - ), - enabled = enabled, - shape = RoundedCornerShape(20.dp), - ) { - Row( - modifier = - Modifier - .padding(all = 16.dp) - .relative(), - verticalAlignment = Alignment.CenterVertically, - ) { - Logo( - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.size(64.dp), - shape = RoundedCornerShape(20), - icon = { - if (module.icon != null) { - AsyncImage( - model = module.icon, - contentScale = ContentScale.Crop, - contentDescription = null, - ) - } else { - Icon( - modifier = it, - painter = painterResource(id = R.drawable.box), - contentDescription = null, - ) - } - }, - ) - - Spacer(modifier = Modifier.width(10.dp)) - - Column( - modifier = Modifier.weight(1f), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - val style = - MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Bold, - ) - - Text( - text = module.name, - style = style, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - if (isVerified) { - Icon( - modifier = Modifier.iconSize(LocalDensity.current, style, 1.0f), - painter = painterResource(id = R.drawable.rosette_discount_check), - contentDescription = null, - tint = MaterialTheme.colorScheme.surfaceTint, - ) - } - } - - Spacer(modifier = Modifier.height(2.dp)) - - Text( - text = module.author, - style = MaterialTheme.typography.bodyMedium.copy(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - maxLines = 1, - text = module.versionDisplay, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.outline, - ) - } - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/TopPicks.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/TopPicks.kt deleted file mode 100644 index b8daa2e20..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/TopPicks.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.repository - -import androidx.annotation.DrawableRes -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ext.nullable -import com.dergoogler.mmrl.model.online.OnlineModule -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListItemSlot -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title -import com.dergoogler.mmrl.ui.providable.LocalOnlineModule - -@Composable -fun TopPicks( - label: String, - @DrawableRes icon: Int? = null, - onMoreClick: () -> Unit, - list: List, -) { - val randomModules = - remember(list) { - list - .shuffled() - .sortedBy { if (it == OnlineModule.example()) 1 else 0 } - } - - val pagerState = - rememberPagerState(pageCount = { (randomModules.size + 2) / 3 }) - - List { - ButtonItem( - onClick = onMoreClick, - ) { - icon.nullable { - Icon( - slot = ListItemSlot.Start, - painter = painterResource(it), - ) - } - Title(label) - Icon( - slot = ListItemSlot.End, - painter = painterResource(R.drawable.arrow_right), - ) - } - - Spacer(Modifier.height(8.dp)) - - HorizontalPager( - state = pagerState, - modifier = - Modifier - .fillMaxWidth() - .animateContentSize(), - contentPadding = PaddingValues(horizontal = 16.dp), - ) { page -> - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 6.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - for (i in 0 until 3) { - val itemIndex = page * 3 + i - if (itemIndex < randomModules.size) { - val item = randomModules[itemIndex] - - CompositionLocalProvider( - LocalOnlineModule provides item, - ) { - if (item.id == "##==online_example==##") { - Spacer(Modifier.height(96.dp)) - return@CompositionLocalProvider - } - - TopPickModule( - modifier = Modifier.height(96.dp), - ) - } - } - } - } - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/modules/TypedModulesList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/modules/TypedModulesList.kt deleted file mode 100644 index c0d5b53f9..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/modules/TypedModulesList.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.repository.modules - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.unit.dp -import com.dergoogler.mmrl.database.entity.Repo -import com.dergoogler.mmrl.datastore.model.RepoListMode -import com.dergoogler.mmrl.model.online.OnlineModule -import com.dergoogler.mmrl.model.state.OnlineState -import com.dergoogler.mmrl.ui.component.scaffold.ScaffoldScope -import com.dergoogler.mmrl.ui.component.scrollbar.VerticalFastScrollbar -import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings -import com.dergoogler.mmrl.ui.providable.LocalOnlineModule -import com.dergoogler.mmrl.ui.providable.LocalOnlineModuleState -import com.dergoogler.mmrl.ui.providable.LocalUserPreferences -import com.dergoogler.mmrl.ui.screens.repository.ModuleItemCompact -import com.dergoogler.mmrl.ui.screens.repository.ModuleItemDetailed -import com.ramcosta.composedestinations.generated.destinations.NewViewScreenDestination -import dev.chrisbanes.haze.hazeSource - -@Composable -fun ScaffoldScope.TypedModulesList( - innerPadding: PaddingValues, - repo: Repo, - list: List>, - state: LazyListState, -) { - val navigator = LocalDestinationsNavigator.current - val userPreferences = LocalUserPreferences.current - val menu = userPreferences.repositoryMenu - - val paddingValues = LocalMainScreenInnerPaddings.current - val layoutDirection = LocalLayoutDirection.current - - val pad = remember(menu) { if (menu.repoListMode == RepoListMode.Compact) 0.dp else 16.dp } - - Box( - modifier = Modifier.fillMaxSize(), - ) { - this@TypedModulesList.ResponsiveContent { - LazyColumn( - state = state, - modifier = Modifier.fillMaxSize().hazeSource(LocalHazeState.current), - contentPadding = - PaddingValues( - top = innerPadding.calculateTopPadding() + pad, - bottom = pad, - start = innerPadding.calculateStartPadding(layoutDirection) + pad, - end = pad, - ), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - items( - items = list, - key = { it.second.id }, - ) { (moduleState, module) -> - CompositionLocalProvider( - LocalOnlineModuleState provides moduleState, - LocalOnlineModule provides module, - ) { - val click = fun() { - navigator.navigate( - NewViewScreenDestination(repo, module), - ) - } - - when (menu.repoListMode) { - RepoListMode.Compact -> - ModuleItemCompact( - onClick = click, - ) - - RepoListMode.Detailed -> - ModuleItemDetailed( - onClick = click, - ) - } - } - } - - item { - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) - } - } - } - - VerticalFastScrollbar( - state = state, - modifier = - Modifier - .align(Alignment.CenterEnd) - .padding( - top = innerPadding.calculateTopPadding(), - bottom = paddingValues.calculateBottomPadding(), - ), - ) - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/modules/TypedModulesScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/modules/TypedModulesScreen.kt deleted file mode 100644 index 03b9d0972..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/repository/modules/TypedModulesScreen.kt +++ /dev/null @@ -1,208 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.repository.modules - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.painterResource -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.database.entity.Repo -import com.dergoogler.mmrl.datastore.model.RepositoryMenu -import com.dergoogler.mmrl.ext.isNotNullOrBlank -import com.dergoogler.mmrl.ext.none -import com.dergoogler.mmrl.ext.rememberSaveableLazyListState -import com.dergoogler.mmrl.ui.component.LocalScreenProvider -import com.dergoogler.mmrl.ui.component.PageIndicator -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.component.toolbar.BlurSearchToolbar -import com.dergoogler.mmrl.ui.component.toolbar.ToolbarTitle -import com.dergoogler.mmrl.ui.remember.rememberOnlineModules -import com.dergoogler.mmrl.ui.remember.rememberUserPreferencesRepository -import com.dergoogler.mmrl.ui.screens.repository.RepositoryMenu -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable - -@Serializable -enum class ModulesFilter { - ALL, - AUTHOR, - CATEGORY, - NAME, -} - -@Destination -@Composable -fun TypedModulesScreen( - title: String? = null, - type: ModulesFilter = ModulesFilter.ALL, - query: String = "", - repo: Repo, -) = LocalScreenProvider { - var searchQuery by remember { mutableStateOf("") } - val modules by rememberOnlineModules(repo, searchQuery) - val userPreferencesRepository = rememberUserPreferencesRepository() - val scope = rememberCoroutineScope() - var isSearch by remember { mutableStateOf(false) } - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - // Use rememberSaveable to persist scroll position across navigation - val listState = rememberSaveableLazyListState(key = "typed_modules_${repo.url}_${type}_${query}") - - val filterQuery = remember { query } - - val filteredModules = - remember(modules, filterQuery, type, searchQuery) { - modules.filter { (_, m) -> - val typeMatches = - when (type) { - ModulesFilter.ALL -> true - ModulesFilter.AUTHOR -> m.author.equals(filterQuery, ignoreCase = true) - ModulesFilter.CATEGORY -> - m.categories?.any { - it.equals( - filterQuery, - ignoreCase = true, - ) - } ?: false - - ModulesFilter.NAME -> m.name.equals(filterQuery, ignoreCase = true) - } - - val searchMatches = - if (searchQuery.isBlank()) { - true - } else { - m.name.contains(searchQuery, ignoreCase = true) || - m.author.contains(searchQuery, ignoreCase = true) || - (m.categories?.any { it.contains(searchQuery, ignoreCase = true) } ?: false) - } - - typeMatches && searchMatches - } - } - - val openSearch = fun() { - isSearch = true - } - - val closeSearch = fun() { - isSearch = false - searchQuery = "" - } - - val search = fun(data: String) { - searchQuery = data - } - - val setRepositoryMenu = fun(value: RepositoryMenu) { - scope.launch { - userPreferencesRepository.setRepositoryMenu(value) - } - } - - BackHandler(enabled = isSearch, onBack = closeSearch) - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopBar( - title = title, - repo = repo, - isSearch = isSearch, - query = searchQuery, - onQueryChange = search, - onOpenSearch = openSearch, - onCloseSearch = closeSearch, - setMenu = setRepositoryMenu, - ) - }, - contentWindowInsets = WindowInsets.none, - ) { innerPadding -> - if (filteredModules.isEmpty()) { - PageIndicator( - icon = R.drawable.cloud, - text = if (isSearch) R.string.search_empty else R.string.repository_empty, - ) - } - - this@Scaffold.TypedModulesList( - innerPadding = innerPadding, - repo = repo, - list = filteredModules, - state = listState, - ) - } -} - -@Composable -private fun TopBar( - title: String?, - repo: Repo, - isSearch: Boolean, - query: String, - onQueryChange: (String) -> Unit, - onOpenSearch: () -> Unit, - onCloseSearch: () -> Unit, - setMenu: (RepositoryMenu) -> Unit, -) { - var currentQuery by remember { mutableStateOf(query) } - DisposableEffect(isSearch) { - onDispose { currentQuery = "" } - } - - BlurSearchToolbar( - isSearch = isSearch, - query = currentQuery, - fade = true, - fadeDistance = 50f, - onQueryChange = { - onQueryChange(it) - currentQuery = it - }, - onClose = { - onCloseSearch() - currentQuery = "" - }, - title = { - if (title.isNotNullOrBlank()) { - ToolbarTitle( - title = title, - subtitle = repo.name, - ) - - return@BlurSearchToolbar - } - - ToolbarTitle( - title = repo.name, - ) - }, - actions = { - if (!isSearch) { - IconButton( - onClick = onOpenSearch, - ) { - Icon( - painter = painterResource(id = R.drawable.search), - contentDescription = null, - ) - } - } - - RepositoryMenu( - setMenu = setMenu, - ) - }, - ) -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/search/SearchScreen.kt index e58f029d1..88c19d370 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/search/SearchScreen.kt @@ -35,8 +35,8 @@ fun SearchScreen() = topBar = { BlurSearchToolbar( isSearch = true, - query = query, autoFocus = false, + query = query, onQueryChange = viewModel::search, title = {}, ) diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/SettingsScreen.kt index dcdf30634..7cbe483c5 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/SettingsScreen.kt @@ -18,14 +18,6 @@ import com.dergoogler.mmrl.model.local.FeaturedManager.Companion.name import com.dergoogler.mmrl.ui.component.SettingsScaffold import com.dergoogler.mmrl.ui.component.dialog.ConfirmData import com.dergoogler.mmrl.ui.component.dialog.rememberConfirm -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListItemSlot -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.RadioDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Icon -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences @@ -33,7 +25,6 @@ import com.jakewharton.processphoenix.ProcessPhoenix import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppearanceScreenDestination -import com.ramcosta.composedestinations.generated.destinations.BlacklistScreenDestination import com.ramcosta.composedestinations.generated.destinations.ChangelogScreenDestination import com.ramcosta.composedestinations.generated.destinations.DeveloperScreenDestination import com.ramcosta.composedestinations.generated.destinations.LogScreenDestination @@ -43,6 +34,13 @@ import com.ramcosta.composedestinations.generated.destinations.SecurityScreenDes import com.ramcosta.composedestinations.generated.destinations.TerminalScreenDestination import com.ramcosta.composedestinations.generated.destinations.UpdatesScreenDestination import com.ramcosta.composedestinations.spec.DirectionDestinationSpec +import dev.mmrlx.compose.ui.list.ListItemSlot +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.list.component.ButtonItem +import dev.mmrlx.compose.ui.list.component.RadioDialogItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Icon +import dev.mmrlx.compose.ui.list.component.item.Title @Destination @Composable @@ -54,151 +52,142 @@ fun SettingsScreen() { val confirm = rememberConfirm(context) SettingsScaffold( - allowNavigateBack = false, + isMainPage = true, title = R.string.page_settings, ) { - List { - context.isPackageInstalled(userPreferences.webuixPackageName).takeFalse { - ButtonItem( - onClick = { - browser.openUri( - if (BuildConfig.IS_GOOGLE_PLAY_BUILD) { - "https://play.google.com/store/apps/details?id=com.dergoogler.mmrl.wx" - } else { - "https://github.com/MMRLApp/WebUI-X-Portable" - }, - ) - }, - ) { - Icon( - painter = painterResource(R.drawable.sandbox), - ) - Title(R.string.settings_try_wxp) - Icon( - slot = ListItemSlot.End, - size = 12.dp, - painter = painterResource(R.drawable.external_link), + context.isPackageInstalled(userPreferences.webuixPackageName).takeFalse { + ButtonItem( + onClick = { + browser.openUri( + if (BuildConfig.IS_GOOGLE_PLAY_BUILD) { + "https://play.google.com/store/apps/details?id=com.dergoogler.mmrl.wx" + } else { + "https://github.com/MMRLApp/WebUI-X-Portable" + }, ) - } + }, + ) { + Icon( + painter = painterResource(R.drawable.sandbox), + ) + Title(R.string.settings_try_wxp) + Icon( + painter = painterResource(R.drawable.external_link), + slot = ListItemSlot.End, + size = 12.dp, + ) } + } - NavButton( - route = AppearanceScreenDestination, - icon = R.drawable.color_swatch, - title = R.string.settings_appearance, - desc = R.string.settings_appearance_desc, - ) - - NavButton( - route = SecurityScreenDestination, - icon = R.drawable.shield, - title = R.string.settings_security, - desc = R.string.settings_security_desc, - ) - - NavButton( - route = UpdatesScreenDestination, - icon = R.drawable.refresh, - title = R.string.settings_updates, - desc = R.string.settings_updates_desc, - ) - - NavButton( - route = ModulesScreen2Destination, - icon = R.drawable.stack_middle, - title = R.string.settings_modules, - desc = R.string.settings_modules_desc, - ) - - NavButton( - route = TerminalScreenDestination, - icon = R.drawable.terminal_2_outlined, - title = R.string.settings_terminal, - desc = R.string.settings_terminal_desc, - ) - - NavButton( - route = OtherScreenDestination, - icon = R.drawable.tool, - title = R.string.settings_other, - desc = R.string.settings_other_desc, - ) - - LinkButton( - uri = Const.RESOURCES_URL, - icon = R.drawable.file_3d, - title = R.string.settings_resources, - desc = R.string.settings_resources_desc, - ) - - val manager = - FeaturedManager.managers.find { userPreferences.workingMode == it.workingMode } - - manager.nullable { mng -> - RadioDialogItem( - selection = mng.workingMode, - options = FeaturedManager.managers.map { it.toRadioDialogItem() }, - onConfirm = { - confirm( - ConfirmData( - title = context.getString(R.string.change_platform), - description = context.getString(R.string.working_mode_change_dialog_desc), - closeText = context.getString(R.string.keep), - onClose = {}, - confirmText = context.getString(R.string.apply), - onConfirm = { - viewModel.setWorkingMode(it.value) - ProcessPhoenix.triggerRebirth(context) - }, - ), - ) - }, - ) { - Icon(painter = painterResource(mng.icon)) - Title(R.string.platform) - Description(mng.name) - } + NavButton( + route = AppearanceScreenDestination, + icon = R.drawable.color_swatch, + title = R.string.settings_appearance, + desc = R.string.settings_appearance_desc, + ) + + NavButton( + route = SecurityScreenDestination, + icon = R.drawable.shield, + title = R.string.settings_security, + desc = R.string.settings_security_desc, + ) + + NavButton( + route = UpdatesScreenDestination, + icon = R.drawable.refresh, + title = R.string.settings_updates, + desc = R.string.settings_updates_desc, + ) + + NavButton( + route = ModulesScreen2Destination, + icon = R.drawable.stack_middle, + title = R.string.settings_modules, + desc = R.string.settings_modules_desc, + ) + + NavButton( + route = TerminalScreenDestination, + icon = R.drawable.terminal_2_outlined, + title = R.string.settings_terminal, + desc = R.string.settings_terminal_desc, + ) + + NavButton( + route = OtherScreenDestination, + icon = R.drawable.tool, + title = R.string.settings_other, + desc = R.string.settings_other_desc, + ) + + LinkButton( + uri = Const.RESOURCES_URL, + icon = R.drawable.file_3d, + title = R.string.settings_resources, + desc = R.string.settings_resources_desc, + ) + + val manager = + FeaturedManager.managers.find { userPreferences.workingMode == it.workingMode } + + manager.nullable { mng -> + RadioDialogItem( + selection = mng.workingMode, + options = FeaturedManager.managers.map { it.toRadioDialogItem() }, + onConfirm = { + confirm( + ConfirmData( + title = context.getString(R.string.change_platform), + description = context.getString(R.string.working_mode_change_dialog_desc), + closeText = context.getString(R.string.keep), + onClose = {}, + confirmText = context.getString(R.string.apply), + onConfirm = { + viewModel.setWorkingMode(it.value) + ProcessPhoenix.triggerRebirth(context) + }, + ), + ) + }, + ) { + Icon(painter = painterResource(mng.icon)) + Title(R.string.platform) + Description(mng.name) } - - NavButton( - route = ChangelogScreenDestination, - icon = R.drawable.files, - title = R.string.settings_changelog, - desc = R.string.settings_changelo_desc, - ) - - NavButton( - route = BlacklistScreenDestination, - icon = R.drawable.file_shredder, - title = R.string.settings_blacklist, - desc = R.string.settings_blacklist_desc, - ) - - NavButton( - route = LogScreenDestination, - icon = R.drawable.logs, - title = R.string.settings_log_viewer, - ) - - NavButton( - route = DeveloperScreenDestination, - icon = R.drawable.bug, - title = R.string.settings_developer, - desc = R.string.settings_developer_desc, - ) - - LinkButton( - uri = Const.PRIVACY_POLICY_URL, - icon = R.drawable.spy, - title = R.string.settings_privacy_policy, - ) - - LinkButton( - uri = Const.TERMS_OF_SERVICE_URL, - icon = R.drawable.files, - title = R.string.settings_terms_of_service, - ) } + + NavButton( + route = ChangelogScreenDestination, + icon = R.drawable.files, + title = R.string.settings_changelog, + desc = R.string.settings_changelo_desc, + ) + + NavButton( + route = LogScreenDestination, + icon = R.drawable.logs, + title = R.string.settings_log_viewer, + ) + + NavButton( + route = DeveloperScreenDestination, + icon = R.drawable.bug, + title = R.string.settings_developer, + desc = R.string.settings_developer_desc, + ) + + LinkButton( + uri = Const.PRIVACY_POLICY_URL, + icon = R.drawable.spy, + title = R.string.settings_privacy_policy, + ) + + LinkButton( + uri = Const.TERMS_OF_SERVICE_URL, + icon = R.drawable.files, + title = R.string.settings_terms_of_service, + ) } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/AppearanceScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/AppearanceScreen.kt index 43a79ec0e..095706acb 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/AppearanceScreen.kt @@ -7,12 +7,6 @@ import com.dergoogler.mmrl.R import com.dergoogler.mmrl.ext.toFormattedDateSafely import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.TextEditDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.DialogDescription -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.ui.screens.settings.NavButton @@ -20,6 +14,12 @@ import com.dergoogler.mmrl.utils.BlurUtil import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppThemeScreenDestination +import dev.mmrlx.compose.ui.list.component.InputDialogItem +import dev.mmrlx.compose.ui.list.component.SwitchItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.DialogDescription +import dev.mmrlx.compose.ui.list.component.item.Supporting +import dev.mmrlx.compose.ui.list.component.item.Title @Destination @Composable @@ -36,10 +36,10 @@ fun AppearanceScreen() { route = AppThemeScreenDestination, ) - TextEditDialogItem( + InputDialogItem( value = userPreferences.datePattern, onConfirm = { - viewModel.setDatePattern(it) + viewModel.setDatePattern(it.value) }, ) { Title(R.string.settings_date_pattern) @@ -70,7 +70,7 @@ fun AppearanceScreen() { Description(R.string.settings_enable_blur_desc) if (!isBlurSupported) { - Labels { + Supporting { LabelItem(text = stringResource(R.string.view_module_unsupported)) } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/items/DownloadPathItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/items/DownloadPathItem.kt index 4ad5501b1..5ebca7e3e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/items/DownloadPathItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/items/DownloadPathItem.kt @@ -1,5 +1,6 @@ package com.dergoogler.mmrl.ui.screens.settings.appearance.items +import androidx.compose.foundation.clickable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -10,15 +11,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.dergoogler.mmrl.R import com.dergoogler.mmrl.app.Const import com.dergoogler.mmrl.ui.component.dialog.TextFieldDialog -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.list.component.RawItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Title import java.io.File @Composable @@ -35,8 +37,12 @@ fun ListScope.DownloadPathItem( ) } - ButtonItem( - onClick = { edit = true }, + RawItem( + modifier = Modifier + .clickable { + edit = true + } + .contentPadding(), ) { Title(R.string.settings_download_path) Description(File(downloadPath).absolutePath) diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/screens/AppThemeScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/screens/AppThemeScreen.kt index ea72678ae..b54fb6fbf 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/screens/AppThemeScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/appearance/screens/AppThemeScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ui.component.ScaffoldDefaults import com.dergoogler.mmrl.ui.component.SettingsScaffold import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences @@ -25,7 +24,6 @@ fun AppThemeScreen() { val viewModel = LocalSettings.current SettingsScaffold( - modifier = ScaffoldDefaults.settingsScaffoldScrollModifier, title = R.string.settings_app_theme, ) { Column( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/blacklist/BlacklistScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/blacklist/BlacklistScreen.kt deleted file mode 100644 index 54ab8c796..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/blacklist/BlacklistScreen.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.settings.blacklist - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.ui.component.PageIndicator -import com.dergoogler.mmrl.ui.component.ScaffoldDefaults -import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings -import com.dergoogler.mmrl.ui.providable.LocalSettings -import com.dergoogler.mmrl.ui.screens.settings.blacklist.items.ModuleItem -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import dev.chrisbanes.haze.hazeSource - -@Destination -@Composable -fun BlacklistScreen() { - val viewModel = LocalSettings.current - val blacklist = viewModel.allBlacklistEntriesAsFlow - - SettingsScaffold( - modifier = ScaffoldDefaults.settingsScaffoldModifier, - title = R.string.settings_blacklist, - ) { - if (blacklist.isEmpty()) { - PageIndicator( - icon = R.drawable.cloud, - text = R.string.search_empty, - ) - } else { - LazyColumn( - modifier = Modifier.hazeSource(LocalHazeState.current), - ) { - items( - items = blacklist, - key = { it.id }, - ) { module -> - ModuleItem(module = module) - } - - item { - val paddingValues = LocalMainScreenInnerPaddings.current - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) - } - } - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/blacklist/items/ModuleItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/blacklist/items/ModuleItem.kt deleted file mode 100644 index 619e20d06..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/blacklist/items/ModuleItem.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.settings.blacklist.items - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.model.online.Blacklist -import com.dergoogler.mmrl.ui.component.BottomSheet -import com.dergoogler.mmrl.ui.component.MarkdownText -import com.dergoogler.mmrl.ui.component.NavigationBarsSpacer -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title - -@Composable -fun ListScope.ModuleItem(module: Blacklist) { - var open by remember { mutableStateOf(false) } - if (open) { - BlacklistBottomSheet( - module = module, - onClose = { open = false }, - ) - } - - ButtonItem( - onClick = { open = true }, - ) { - Title(module.id) - Description(module.source) - } -} - -@Composable -fun BlacklistBottomSheet( - module: Blacklist, - onClose: () -> Unit, -) = BottomSheet(onDismissRequest = onClose) { - val browser = LocalUriHandler.current - - Column( - modifier = Modifier.padding(bottom = 18.dp), - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - Text( - style = MaterialTheme.typography.titleLarge, - text = module.id, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - module.notes?.let { - if (it.isNotEmpty()) { - Surface( - color = MaterialTheme.colorScheme.surface, - tonalElevation = 1.dp, - shape = RoundedCornerShape(20.dp), - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - Text( - text = stringResource(R.string.additional_notes), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 4.dp), - ) - MarkdownText( - text = it, - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline), - ) - } - } - - Spacer(modifier = Modifier.height(4.dp)) - } - } - } - - Spacer(modifier = Modifier.height(10.dp)) - - Button( - modifier = - Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - onClick = { - browser.openUri(module.source) - }, - ) { - Text(stringResource(R.string.open_source)) - } - - NavigationBarsSpacer() - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/ChangelogScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/ChangelogScreen.kt index adf82b5ec..d6689bf88 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/ChangelogScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/ChangelogScreen.kt @@ -3,8 +3,6 @@ package com.dergoogler.mmrl.ui.screens.settings.changelogs import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable @@ -13,21 +11,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import com.dergoogler.mmrl.R import com.dergoogler.mmrl.model.online.Changelog import com.dergoogler.mmrl.network.runRequest import com.dergoogler.mmrl.stub.IMMRLApiManager import com.dergoogler.mmrl.ui.component.Loading -import com.dergoogler.mmrl.ui.component.ScaffoldDefaults import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.ui.screens.settings.changelogs.items.ChangelogItem import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph -import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber @@ -52,7 +45,7 @@ fun ChangelogScreen() { } SettingsScaffold( - modifier = ScaffoldDefaults.settingsScaffoldModifier, + canScroll = false, title = R.string.settings_changelog, ) { AnimatedVisibility( @@ -69,19 +62,12 @@ fun ChangelogScreen() { exit = fadeOut(), ) { changelog?.let { bl -> - LazyColumn( - modifier = Modifier.hazeSource(LocalHazeState.current), - ) { + LazyColumn { items(items = bl, key = { it.versionCode }) { entry -> if (!userPreferences.checkAppUpdatesPreReleases && entry.preRelease) return@items ChangelogItem(changelog = entry) } - - item { - val paddingValues = LocalMainScreenInnerPaddings.current - Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding())) - } } } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/items/ChangelogItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/items/ChangelogItem.kt index f3f30dd3c..ab89bca1b 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/items/ChangelogItem.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/changelogs/items/ChangelogItem.kt @@ -1,5 +1,6 @@ package com.dergoogler.mmrl.ui.screens.settings.changelogs.items +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -28,11 +29,11 @@ import com.dergoogler.mmrl.model.online.Changelog import com.dergoogler.mmrl.ui.component.BottomSheet import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.MarkdownText -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.list.component.RawItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Supporting +import dev.mmrlx.compose.ui.list.component.item.Title @Composable fun ListScope.ChangelogItem(changelog: Changelog) { @@ -41,13 +42,17 @@ fun ListScope.ChangelogItem(changelog: Changelog) { ChangelogBottomSheet(changelog = changelog, onClose = { open = false }) } - ButtonItem( - onClick = { open = true }, + RawItem( + modifier = Modifier + .clickable { + open = true + } + .contentPadding(), ) { Title(changelog.versionName) Description(changelog.versionCode.toString()) changelog.preRelease.takeTrue { - Labels { + Supporting { LabelItem( text = stringResource( diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/developer/DeveloperScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/developer/DeveloperScreen.kt index feb65143b..b367c4e4e 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/developer/DeveloperScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/developer/DeveloperScreen.kt @@ -5,13 +5,13 @@ import androidx.compose.ui.res.stringResource import com.dergoogler.mmrl.BuildConfig import com.dergoogler.mmrl.R import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListItemScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Item -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Section -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.ListItemScope +import dev.mmrlx.compose.ui.list.ListScope +import dev.mmrlx.compose.ui.list.component.Item +import dev.mmrlx.compose.ui.list.component.Section +import dev.mmrlx.compose.ui.list.component.SwitchItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.ramcosta.composedestinations.annotation.Destination diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/logviewer/LogScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/logviewer/LogScreen.kt index 8e202aed4..9c0b235f8 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/logviewer/LogScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/logviewer/LogScreen.kt @@ -27,8 +27,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -69,6 +67,8 @@ import com.dergoogler.mmrl.utils.log.Logcat import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import dev.chrisbanes.haze.hazeSource +import dev.mmrlx.compose.ui.toolbar.ToolbarDefaults +import dev.mmrlx.compose.ui.toolbar.ToolbarScrollBehavior import kotlinx.coroutines.launch private val priorities = listOf("VERBOSE", "DEBUG", "INFO", "WARN", "ERROR") @@ -116,7 +116,7 @@ object LogColors { fun LogScreen() = LocalScreenProvider { val context = LocalContext.current - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val scrollBehavior = ToolbarDefaults.pinnedScrollBehavior() val state = rememberLazyListState() var priority by remember { mutableStateOf("DEBUG") } @@ -205,7 +205,7 @@ fun LogScreen() = private fun TopBar( priority: String, onPriority: (String) -> Unit, - scrollBehavior: TopAppBarScrollBehavior, + scrollBehavior: ToolbarScrollBehavior, ) = BlurNavigateUpToolbar( title = stringResource(id = R.string.settings_log_viewer), navController = LocalNavController.current, diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/modules/ModulesScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/modules/ModulesScreen.kt index 6ca26792c..ba1b18748 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/modules/ModulesScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/modules/ModulesScreen.kt @@ -7,16 +7,17 @@ import com.dergoogler.mmrl.datastore.model.WebUIEngine import com.dergoogler.mmrl.ui.component.APatchLabel import com.dergoogler.mmrl.ui.component.KernelSuLabel import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.RadioDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Section -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import dev.mmrlx.compose.ui.list.component.RadioDialogItem +import dev.mmrlx.compose.ui.list.component.RadioDialogOption +import dev.mmrlx.compose.ui.list.component.Section +import dev.mmrlx.compose.ui.list.component.SwitchItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Supporting +import dev.mmrlx.compose.ui.list.component.item.Title @Destination @Composable @@ -37,7 +38,7 @@ fun ModulesScreen2() { ) { Title(R.string.settings_shell_module_state_change) Description(R.string.settings_shell_module_state_change_desc) - Labels { + Supporting { KernelSuLabel() APatchLabel() } @@ -50,7 +51,7 @@ fun ModulesScreen2() { ) { Title(R.string.settings_use_generic_action) Description(R.string.settings_use_generic_action_desc) - Labels { + Supporting { KernelSuLabel() APatchLabel() } @@ -66,15 +67,15 @@ fun ModulesScreen2() { selection = userPreferences.webuiEngine, options = listOf( - RadioDialogItem( + RadioDialogOption( value = WebUIEngine.WX, title = stringResource(R.string.settings_webui_engine_wx), ), - RadioDialogItem( + RadioDialogOption( value = WebUIEngine.KSU, title = stringResource(R.string.settings_webui_engine_ksu), ), - RadioDialogItem( + RadioDialogOption( value = WebUIEngine.PREFER_MODULE, title = stringResource(R.string.settings_webui_engine_prefer_module), ), diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/other/OtherScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/other/OtherScreen.kt index 12077de1f..65d488c83 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/other/OtherScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/other/OtherScreen.kt @@ -6,16 +6,16 @@ import androidx.compose.ui.platform.LocalContext import com.dergoogler.mmrl.R import com.dergoogler.mmrl.service.ProviderService import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.TextEditDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.component.SwitchItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalSnackbarHost import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.dergoogler.mmrl.ui.screens.settings.appearance.items.DownloadPathItem import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import dev.mmrlx.compose.ui.list.component.InputDialogItem import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -67,12 +67,14 @@ fun OtherScreen() { Description(R.string.settings_provider_service_desc) } - TextEditDialogItem( + InputDialogItem( value = userPreferences.webuixPackageName, - onConfirm = viewModel::setWebuixPackageName, + onConfirm = { + viewModel.setWebuixPackageName(it.value) + }, ) { - Title(context.getString(R.string.settings_set_spoofed_wxp)) - Description(context.getString(R.string.settings_set_spoofed_wxp_desc)) + Title(R.string.settings_set_spoofed_wxp) + Description(R.string.settings_set_spoofed_wxp_desc) } } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/security/SecurityScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/security/SecurityScreen.kt index 94073545f..a238e1ed7 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/security/SecurityScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/security/SecurityScreen.kt @@ -11,14 +11,14 @@ import com.dergoogler.mmrl.R import com.dergoogler.mmrl.platform.ksu.KsuNative import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import dev.mmrlx.compose.ui.list.component.SwitchItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Supporting +import dev.mmrlx.compose.ui.list.component.item.Title @Destination @Composable @@ -83,7 +83,7 @@ fun SecurityScreen() { Description(R.string.settings_disable_su_desc) if (!isSuDisableSupported) { - Labels { + Supporting { LabelItem(text = stringResource(R.string.view_module_unsupported)) } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/terminal/TerminalScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/terminal/TerminalScreen.kt index e3ad72348..9c982efc8 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/terminal/TerminalScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/terminal/TerminalScreen.kt @@ -5,10 +5,10 @@ import androidx.compose.ui.res.stringResource import com.dergoogler.mmrl.R import com.dergoogler.mmrl.datastore.model.WorkingMode.Companion.isRoot import com.dergoogler.mmrl.ui.component.SettingsScaffold -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Section -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.component.Section +import dev.mmrlx.compose.ui.list.component.SwitchItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.ramcosta.composedestinations.annotation.Destination diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/updates/UpdatesScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/updates/UpdatesScreen.kt index d061b2831..21257d6e4 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/updates/UpdatesScreen.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/settings/updates/UpdatesScreen.kt @@ -3,8 +3,10 @@ package com.dergoogler.mmrl.ui.screens.settings.updates import android.content.Intent import android.provider.Settings import android.widget.Toast +import androidx.compose.foundation.clickable import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -15,21 +17,25 @@ import com.dergoogler.mmrl.service.RepositoryService import com.dergoogler.mmrl.ui.component.SettingsScaffold import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem import com.dergoogler.mmrl.ui.component.listItem.dsl.component.RadioDialogItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.Section -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.SwitchItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title +import dev.mmrlx.compose.ui.list.component.RadioDialogItem +import dev.mmrlx.compose.ui.list.component.Section +import dev.mmrlx.compose.ui.list.component.SwitchItem +import dev.mmrlx.compose.ui.list.component.item.Description +import dev.mmrlx.compose.ui.list.component.item.Title import com.dergoogler.mmrl.ui.providable.LocalSettings import com.dergoogler.mmrl.ui.providable.LocalSnackbarHost import com.dergoogler.mmrl.ui.providable.LocalUserPreferences import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph +import dev.mmrlx.compose.ui.list.component.RadioDialogOption +import dev.mmrlx.compose.ui.list.component.RawItem import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds @Composable -fun radioDialogItem(interval: Long): RadioDialogItem = - RadioDialogItem( +fun radioDialogItem(interval: Long): RadioDialogOption = + RadioDialogOption( title = pluralStringResource(id = R.plurals.hours, count = interval.toInt(), interval), value = interval, ) @@ -63,24 +69,26 @@ fun UpdatesScreen() { title = R.string.settings_updates, ) { Section { - ButtonItem( - onClick = { - val intent = - Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - } + RawItem( + modifier = Modifier + .clickable { + val intent = + Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } - if (intent.resolveActivity(context.packageManager) != null) { - context.startActivity(intent, null) - } else { - Toast - .makeText( - context, - "Cannot open notification settings", - Toast.LENGTH_SHORT, - ).show() + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent, null) + } else { + Toast + .makeText( + context, + "Cannot open notification settings", + Toast.LENGTH_SHORT, + ).show() + } } - }, + .contentPadding(), ) { Title(R.string.settings_open_notification_settings) } @@ -123,7 +131,7 @@ fun UpdatesScreen() { } else { RepositoryService.stop(context) while (RepositoryService.isActive) { - delay(100) + delay(100.milliseconds) } viewModel.setRepositoryServiceEnabled(false) snackbarHost.showSnackbar(context.getString(R.string.repository_service_stopped)) @@ -144,8 +152,10 @@ fun UpdatesScreen() { ) { Title(R.string.settings_repo_update_interval) Description( - R.string.settings_repo_update_interval_desc, - userPreferences.autoUpdateReposInterval, + stringResource( + R.string.settings_repo_update_interval_desc, + userPreferences.autoUpdateReposInterval + ), ) } } @@ -190,8 +200,10 @@ fun UpdatesScreen() { ) { Title(R.string.settings_check_modules_update_interval) Description( - R.string.settings_check_modules_update_interval_desc, - userPreferences.checkModuleUpdatesInterval, + stringResource( + R.string.settings_check_modules_update_interval_desc, + userPreferences.checkModuleUpdatesInterval + ), ) } } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserItem.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserItem.kt deleted file mode 100644 index 26affa14b..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserItem.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.superuser - -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import com.dergoogler.mmrl.platform.ksu.KsuNative -import com.dergoogler.mmrl.ui.component.LabelItem -import com.dergoogler.mmrl.ui.component.LabelItemDefaults -import com.dergoogler.mmrl.ui.component.listItem.dsl.ListScope -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.ButtonItem -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Description -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Labels -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Start -import com.dergoogler.mmrl.ui.component.listItem.dsl.component.item.Title -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel.AppInfo.Companion.loadIcon - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun ListScope.SuperUserItem( - app: SuperUserViewModel.AppInfo, - onClick: () -> Unit, -) { - val context = LocalContext.current - - ButtonItem( - onClick = onClick, - ) { - Title(app.label) - Description(app.packageName) - - Start { - AsyncImage( - model = app.loadIcon(context), - contentDescription = app.label, - modifier = Modifier.size(48.dp), - ) - } - - Labels { - if (app.allowSu) { - LabelItem( - text = "ROOT", - ) - } else { - if (KsuNative.uidShouldUmount(app.uid)) { - LabelItem( - text = "UMOUNT", - style = - LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer, - ), - ) - } - } - if (app.hasCustomProfile) { - LabelItem( - text = "CUSTOM", - style = - LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - ), - ) - } else if (!app.allowSu && !KsuNative.uidShouldUmount(app.uid)) { - LabelItem( - text = "DEFAULT", - style = - LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - ) - } - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserList.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserList.kt deleted file mode 100644 index 76ddbdcee..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserList.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.superuser - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.unit.dp -import com.dergoogler.mmrl.ui.component.listItem.dsl.List -import com.dergoogler.mmrl.ui.component.scaffold.ScaffoldScope -import com.dergoogler.mmrl.ui.providable.LocalDestinationsNavigator -import com.dergoogler.mmrl.ui.providable.LocalHazeState -import com.dergoogler.mmrl.ui.providable.LocalMainScreenInnerPaddings -import com.dergoogler.mmrl.viewmodel.SuperUserViewModel -import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination -import dev.chrisbanes.haze.hazeSource - -@Composable -fun ScaffoldScope.SuperUserList( - innerPadding: PaddingValues, - list: List, - state: LazyListState, -) { - val navigator = LocalDestinationsNavigator.current - val paddingValues = LocalMainScreenInnerPaddings.current - val layoutDirection = LocalLayoutDirection.current - - this@SuperUserList.ResponsiveContent { - List { - LazyColumn( - state = state, - modifier = - Modifier - .fillMaxSize() - .hazeSource(state = LocalHazeState.current), - contentPadding = - PaddingValues( - top = innerPadding.calculateTopPadding(), - start = innerPadding.calculateStartPadding(layoutDirection), - bottom = paddingValues.calculateBottomPadding(), - end = 0.dp, - ), - ) { - items( - items = list, - key = { it.packageName + it.uid }, - ) { app -> - SuperUserItem(app) { - navigator.navigate(AppProfileScreenDestination(app)) - } - } - } - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserMenu.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserMenu.kt deleted file mode 100644 index 22d71a528..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserMenu.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.superuser - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.datastore.model.SuperUserMenu -import com.dergoogler.mmrl.ui.component.BottomSheet -import com.dergoogler.mmrl.ui.component.MenuChip -import com.dergoogler.mmrl.ui.providable.LocalUserPreferences - -@Composable -fun SuperUserMenu(setMenu: (SuperUserMenu) -> Unit) { - val userPreferences = LocalUserPreferences.current - var open by rememberSaveable { mutableStateOf(false) } - - IconButton( - onClick = { open = true }, - ) { - Icon( - painter = painterResource(id = R.drawable.filter_outlined), - contentDescription = null, - ) - - if (open) { - MenuBottomSheet( - onClose = { open = false }, - menu = userPreferences.superUserMenu, - setMenu = setMenu, - ) - } - } -} - -@Composable -private fun MenuBottomSheet( - onClose: () -> Unit, - menu: SuperUserMenu, - setMenu: (SuperUserMenu) -> Unit, -) = BottomSheet(onDismissRequest = onClose) { - Text( - text = stringResource(id = R.string.menu_advanced_menu), - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.align(Alignment.CenterHorizontally), - ) - - Column( - modifier = Modifier.padding(all = 18.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(id = R.string.menu_sort_mode), - style = MaterialTheme.typography.titleSmall, - ) - - FlowRow( - modifier = - Modifier - .fillMaxWidth(1f) - .wrapContentHeight(align = Alignment.Top), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - /*MenuChip( - selected = menu.pinHasRoot, - onClick = { setMenu(menu.copy(pinHasRoot = !menu.pinHasRoot)) }, - label = { Text(text = stringResource(id = R.string.menu_pin_installed)) } - )*/ - - MenuChip( - selected = menu.showSystemApps, - onClick = { setMenu(menu.copy(showSystemApps = !menu.showSystemApps)) }, - label = { Text(text = stringResource(id = R.string.menu_show_system_apps)) }, - ) - } - } -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserScreen.kt b/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserScreen.kt deleted file mode 100644 index 2d1f727d3..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/ui/screens/superuser/SuperUserScreen.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.dergoogler.mmrl.ui.screens.superuser - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.painterResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.dergoogler.mmrl.R -import com.dergoogler.mmrl.datastore.model.SuperUserMenu -import com.dergoogler.mmrl.ext.currentScreenWidth -import com.dergoogler.mmrl.ext.none -import com.dergoogler.mmrl.ui.component.Loading -import com.dergoogler.mmrl.ui.component.LocalScreenProvider -import com.dergoogler.mmrl.ui.component.PageIndicator -import com.dergoogler.mmrl.ui.component.TopAppBarEventIcon -import com.dergoogler.mmrl.ui.component.scaffold.Scaffold -import com.dergoogler.mmrl.ui.component.toolbar.BlurSearchToolbar -import com.dergoogler.mmrl.ui.providable.LocalSuperUserViewModel -import com.dergoogler.mmrl.ui.providable.LocalUserPreferences -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph - -@Destination -@Composable -fun SuperUserScreen() = - LocalScreenProvider { - val userPrefs = LocalUserPreferences.current - val viewModel = LocalSuperUserViewModel.current - - val list by viewModel.local.collectAsStateWithLifecycle() - val query by viewModel.query.collectAsStateWithLifecycle() - val state by viewModel.screenState.collectAsStateWithLifecycle() - val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - - val pullToRefreshState = rememberPullToRefreshState() - - BackHandler( - enabled = viewModel.isSearch, - onBack = viewModel::closeSearch, - ) - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopBar( - isSearch = viewModel.isSearch, - query = query, - onQueryChange = viewModel::search, - onOpenSearch = viewModel::openSearch, - onCloseSearch = viewModel::closeSearch, - setMenu = viewModel::setSuperUserMenu, - scrollBehavior = scrollBehavior, - ) - }, - contentWindowInsets = WindowInsets.none, - ) { innerPadding -> - if (isLoading) { - Loading() - } - - if (list.isEmpty() && !isLoading) { - PageIndicator( - icon = R.drawable.keyframes, - text = if (viewModel.isSearch) R.string.search_empty else R.string.modules_empty, - ) - } - - PullToRefreshBox( - state = pullToRefreshState, - isRefreshing = state.isRefreshing, - onRefresh = viewModel::fetchAppList, - indicator = { - Indicator( - modifier = - Modifier.align(Alignment.TopCenter).let { - if (!userPrefs.enableBlur) { - it.padding(top = innerPadding.calculateTopPadding()) - } else { - it - } - }, - isRefreshing = state.isRefreshing, - state = pullToRefreshState, - ) - }, - ) { - this@Scaffold.SuperUserList( - innerPadding = innerPadding, - list = list, - state = viewModel.listState, - ) - } - } - } - -@Composable -private fun TopBar( - isSearch: Boolean, - query: String, - onQueryChange: (String) -> Unit, - onOpenSearch: () -> Unit, - onCloseSearch: () -> Unit, - setMenu: (SuperUserMenu) -> Unit, - scrollBehavior: TopAppBarScrollBehavior, -) { - val width = currentScreenWidth() - - var currentQuery by remember { mutableStateOf(query) } - DisposableEffect(isSearch) { - onDispose { currentQuery = "" } - } - - BlurSearchToolbar( - scrollBehavior = scrollBehavior, - isSearch = isSearch, - query = currentQuery, - fade = true, - fadeDistance = 50f, - onQueryChange = { - onQueryChange(it) - currentQuery = it - }, - onClose = { - onCloseSearch() - currentQuery = "" - }, - title = { - if (!width.isSmall) return@BlurSearchToolbar - - TopAppBarEventIcon() - }, - actions = { - if (!isSearch) { - IconButton( - onClick = onOpenSearch, - ) { - Icon( - painter = painterResource(id = R.drawable.search), - contentDescription = null, - ) - } - } - - SuperUserMenu( - setMenu = setMenu, - ) - }, - ) -} diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/Extensions.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/Extensions.kt index 7d027432a..9e29630b8 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/utils/Extensions.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/Extensions.kt @@ -1,27 +1,8 @@ package com.dergoogler.mmrl.utils -import android.annotation.SuppressLint -import android.content.Context -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable -import com.dergoogler.mmrl.App -import com.dergoogler.mmrl.BuildConfig -import com.dergoogler.mmrl.datastore.model.UserPreferences -import com.dergoogler.mmrl.datastore.model.WebUIEngine -import com.dergoogler.mmrl.ext.findActivity import com.dergoogler.mmrl.ext.toFormattedDateSafely -import com.dergoogler.mmrl.modconf.helper.ModConfLauncher -import com.dergoogler.mmrl.model.local.LocalModule -import com.dergoogler.mmrl.platform.PlatformManager -import com.dergoogler.mmrl.platform.content.LocalModule.Companion.hasModConf -import com.dergoogler.mmrl.platform.content.LocalModule.Companion.hasWebUI -import com.dergoogler.mmrl.platform.ksu.KsuNative -import com.dergoogler.mmrl.platform.model.toModuleConfig import com.dergoogler.mmrl.ui.providable.LocalUserPreferences -import com.dergoogler.mmrl.webui.helper.WebUILauncher import com.topjohnwu.superuser.NoShellException import com.topjohnwu.superuser.Shell @@ -60,119 +41,3 @@ fun createRootShell( } return builder.build(*commands) } - -@SuppressLint("MissingPermission") -@Composable -fun UserPreferences.webUILauncher( - context: Context, - module: LocalModule, -): () -> Unit { - val modId = module.id - val config = modId.toModuleConfig() - - val activity = - context.findActivity() as? ComponentActivity ?: run { - Toast.makeText(context, "No activity found", Toast.LENGTH_SHORT).show() - return {} - } - - val modconfLauncher = - ModConfLauncher( - debug = BuildConfig.DEBUG, - packageName = webuixPackageName, - ) - - val webuiLauncher = - WebUILauncher( - debug = BuildConfig.DEBUG, - packageName = webuixPackageName, - ) - - fun Map.allGranted(onDenied: (String) -> Unit): Boolean { - for ((perm, granted) in this) { - if (!granted) { - onDenied(perm) - return false - } - } - return true - } - - val modconf = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestMultiplePermissions(), - ) { result -> - if (!result.allGranted { perm -> - Toast - .makeText(context, "Permission denied! Requires $perm", Toast.LENGTH_SHORT) - .show() - } - ) { - return@rememberLauncherForActivityResult - } - - modconfLauncher.launch( - context = context, - modId = modId, - platform = workingMode.toPlatform(), - ) - } - - val webui = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestMultiplePermissions(), - ) { result -> - if (!result.allGranted { perm -> - Toast - .makeText(context, "Permission denied! Requires $perm", Toast.LENGTH_SHORT) - .show() - } - ) { - return@rememberLauncherForActivityResult - } - - val effectiveEngine = - when (webuiEngine) { - WebUIEngine.PREFER_MODULE -> - config.getWebuiEngine(context)?.let { - when (it) { - "wx" -> WebUIEngine.WX - "ksu" -> WebUIEngine.KSU - else -> { - Toast.makeText(context, "Unknown WebUI engine", Toast.LENGTH_SHORT).show() - return@rememberLauncherForActivityResult - } - } - } ?: WebUIEngine.WX - - else -> webuiEngine - } - - when (effectiveEngine) { - WebUIEngine.WX -> webuiLauncher.launchWX(context, modId, workingMode.toPlatform()) - WebUIEngine.KSU -> webuiLauncher.launchLegacy(context, modId, workingMode.toPlatform()) - else -> Toast.makeText(context, "Unsupported WebUI engine", Toast.LENGTH_SHORT).show() - } - } - - return { - when { - module.hasModConf -> modconf.launch(arrayOf(modconfLauncher.permissions.MODCONF)) - module.hasWebUI -> - webui.launch( - arrayOf( - webuiLauncher.permissions.WEBUI_X, - webuiLauncher.permissions.WEBUI_LEGACY, - ), - ) - - else -> Toast.makeText(context, "Unsupported module", Toast.LENGTH_SHORT).show() - } - } -} - -val KsuNative.isManager: Boolean - get() { - val pkg = App.context.packageName - return PlatformManager.platform.isKernelSuVariant && becomeManager(pkg) - } diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/utils/MimeUtil.kt b/app/src/main/kotlin/com/dergoogler/mmrl/utils/MimeUtil.kt new file mode 100644 index 000000000..9ca504ed0 --- /dev/null +++ b/app/src/main/kotlin/com/dergoogler/mmrl/utils/MimeUtil.kt @@ -0,0 +1,63 @@ +package com.dergoogler.mmrl.utils + +import java.net.URLConnection +import java.util.Locale + +object MimeUtil { + fun getMimeFromFileName(fileName: String?): String? { + if (fileName == null) { + return null + } + + val mimeType = URLConnection.guessContentTypeFromName(fileName) + + if (mimeType != null) { + return mimeType + } + + return guessHardcodedMime(fileName) + } + + private fun guessHardcodedMime(fileName: String): String? { + val finalFullStop = fileName.lastIndexOf('.') + if (finalFullStop == -1) { + return null + } + + val extension = fileName.substring(finalFullStop + 1).lowercase(Locale.getDefault()) + + return when (extension) { + "webm" -> "video/webm" + "mpeg", "mpg" -> "video/mpeg" + "mp3" -> "audio/mpeg" + "wasm" -> "application/wasm" + "xhtml", "xht", "xhtm" -> "application/xhtml+xml" + "flac" -> "audio/flac" + "ogg", "oga", "opus" -> "audio/ogg" + "wav" -> "audio/wav" + "m4a" -> "audio/x-m4a" + "gif" -> "image/gif" + "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg" + "png" -> "image/png" + "apng" -> "image/apng" + "svg", "svgz" -> "image/svg+xml" + "webp" -> "image/webp" + "mht", "mhtml" -> "multipart/related" + "css" -> "text/css" + "html", "htm", "shtml", "shtm", "ehtml" -> "text/html" + "js", "mjs" -> "application/javascript" + "xml" -> "text/xml" + "mp4", "m4v" -> "video/mp4" + "ogv", "ogm" -> "video/ogg" + "ico" -> "image/x-icon" + "woff" -> "application/font-woff" + "gz", "tgz" -> "application/gzip" + "json" -> "application/json" + "pdf" -> "application/pdf" + "zip" -> "application/zip" + "bmp" -> "image/bmp" + "tiff", "tif" -> "image/tiff" + else -> null + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoryViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoryViewModel.kt index c8371ba8a..094be327b 100644 --- a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoryViewModel.kt +++ b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/RepositoryViewModel.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewModelScope import com.dergoogler.mmrl.database.entity.Repo import com.dergoogler.mmrl.datastore.UserPreferencesRepository @@ -23,192 +22,190 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import timber.log.Timber @HiltViewModel(assistedFactory = RepositoryViewModel.Factory::class) class RepositoryViewModel - @AssistedInject - constructor( - @Assisted val repo: Repo, - application: Application, - localRepository: LocalRepository, - modulesRepository: ModulesRepository, - userPreferencesRepository: UserPreferencesRepository, - ) : MMRLViewModel(application, localRepository, modulesRepository, userPreferencesRepository) { - private val repositoryMenu - get() = - userPreferencesRepository.data - .map { it.repositoryMenu } - - var isSearch by mutableStateOf(false) - private set - private val keyFlow = MutableStateFlow("") - val query get() = keyFlow.asStateFlow() - - val listState: LazyListState = LazyListState() - - private val cacheFlow = MutableStateFlow(listOf>()) - private val onlineFlow = MutableStateFlow(listOf>()) - val online get() = onlineFlow.asStateFlow() - - var isLoading by mutableStateOf(true) - private set - - init { - Timber.d("RepositoryViewModel init") - dataObserver() - keyObserver() +@AssistedInject +constructor( + @Assisted val repo: Repo, + application: Application, + localRepository: LocalRepository, + modulesRepository: ModulesRepository, + userPreferencesRepository: UserPreferencesRepository, +) : MMRLViewModel( + application, + localRepository, + modulesRepository, + userPreferencesRepository, +) { + private val keyFlow = MutableStateFlow("") + val query: StateFlow = keyFlow.asStateFlow() + + private val repositoryMenu = + userPreferencesRepository.data.map { it.repositoryMenu } + + val listState: LazyListState = LazyListState() + + var isSearch by mutableStateOf(false) + private set + + var isLoading by mutableStateOf(true) + private set + + private val onlineModules = + if (repo.url.isNotBlank()) { + localRepository.getOnlineAllByUrlAsFlow(repo.url) + } else { + localRepository.getOnlineAllAsFlow() } - private fun dataObserver() { - val onlineModules = - if (repo.url.isNotBlank()) { - localRepository.getOnlineAllByUrlAsFlow(repo.url) - } else { - localRepository.getOnlineAllAsFlow() - } + val online: StateFlow>> = + combine( + onlineModules, + repositoryMenu, + keyFlow, + ) { modules, menu, key -> + val source = modules.map { module -> + val local = localRepository.getLocalByIdOrNull(module.id) - combine( - onlineModules, - repositoryMenu, - ) { list, menu -> - cacheFlow.value = - list - .map { - val local = localRepository.getLocalByIdOrNull(it.id) - val versionsList = it.versions.toMutableList() - - if (local != null) { - UpdateJson.loadToVersionItem(local.updateJson)?.let { es -> - // no need to define here a repo name since we only need to for the last updated - versionsList.add(0, es) - } - } - - it.copy(versions = versionsList).createState( - local = local, - hasUpdatableTag = localRepository.hasUpdatableTag(it.id), - ) to it.copy(versions = versionsList) - }.sortedWith( - comparator(menu.option, menu.descending), - ).let { v -> - val a = - if (menu.pinInstalled) { - v.sortedByDescending { it.first.installed } - } else { - v - } - - if (menu.pinUpdatable) { - a.sortedByDescending { it.first.updatable } - } else { - a - } - } - - isLoading = false - }.launchIn(viewModelScope) - } + val versions = module.versions.toMutableList() - private fun keyObserver() { - combine( - keyFlow, - cacheFlow, - ) { key, source -> - val newKey = - when { - key.startsWith("id:", ignoreCase = true) -> key.removePrefix("id:") - key.startsWith("name:", ignoreCase = true) -> key.removePrefix("name:") - key.startsWith("author:", ignoreCase = true) -> key.removePrefix("author:") - key.startsWith("category:", ignoreCase = true) -> key.removePrefix("category:") - else -> key - }.trim() - - onlineFlow.value = - source.filter { (_, m) -> - if (key.isNotBlank() || newKey.isNotBlank()) { - when { - key.startsWith("id:", ignoreCase = true) -> - m.id.equals(newKey, ignoreCase = true) - - key.startsWith("name:", ignoreCase = true) -> - m.name.equals(newKey, ignoreCase = true) - - key.startsWith("author:", ignoreCase = true) -> - m.author.equals(newKey, ignoreCase = true) - - key.startsWith("category:", ignoreCase = true) -> - m.categories?.any { - it.equals( - newKey, - ignoreCase = true, - ) - } ?: false - - else -> - m.name.contains(key, ignoreCase = true) || - m.author.contains(key, ignoreCase = true) || - m.description?.contains(key, ignoreCase = true) == true - } - } else { - true - } + if (local != null) { + UpdateJson.loadToVersionItem(local.updateJson)?.let { + versions.add(0, it) } - }.launchIn(viewModelScope) - } + } - private fun comparator( - option: Option, - descending: Boolean, - ): Comparator> = - if (descending) { - when (option) { - Option.Name -> compareByDescending { it.second.name.lowercase() } - Option.UpdatedTime -> compareBy { it.first.lastUpdated } - Option.Size -> compareByDescending { 0 } + val updatedModule = module.copy(versions = versions) + + updatedModule.createState( + local = local, + hasUpdatableTag = localRepository.hasUpdatableTag(module.id), + ) to updatedModule + } + + val sorted = source + .sortedWith(comparator(menu.option, menu.descending)) + .let { list -> + if (menu.pinInstalled) { + list.sortedByDescending { it.first.installed } + } else { + list + } } + .let { list -> + if (menu.pinUpdatable) { + list.sortedByDescending { it.first.updatable } + } else { + list + } + } + + isLoading = false + + if (key.isBlank()) { + sorted } else { - when (option) { - Option.Name -> compareBy { it.second.name.lowercase() } - Option.UpdatedTime -> compareByDescending { it.first.lastUpdated } - Option.Size -> compareBy { 0 } + val (searchKey, prefix) = parseSearchKey(key) + + sorted.filter { (_, module) -> + when (prefix) { + "id" -> + module.id.equals(searchKey, ignoreCase = true) + + "name" -> + module.name.equals(searchKey, ignoreCase = true) + + "author" -> + module.author.equals(searchKey, ignoreCase = true) + + "category" -> + module.categories?.any { + it.equals(searchKey, ignoreCase = true) + } == true + + else -> + module.name.contains(searchKey, ignoreCase = true) || + module.author.contains(searchKey, ignoreCase = true) || + module.description?.contains( + searchKey, + ignoreCase = true, + ) == true + } } } - - fun search(key: String) { - keyFlow.value = key + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + private fun parseSearchKey(raw: String): Pair { + val prefix = listOf("id", "name", "author", "category") + .firstOrNull { raw.startsWith("$it:", ignoreCase = true) } + + return if (prefix != null) { + raw.substringAfter(":").trim() to prefix + } else { + raw.trim() to null } + } - fun openSearch() { - isSearch = true - } + private fun comparator( + option: Option, + descending: Boolean, + ): Comparator> { + val base: Comparator> = + when (option) { + Option.Name -> + compareBy { it.second.name.lowercase() } - fun closeSearch() { - isSearch = false - keyFlow.value = "" - } + Option.UpdatedTime -> + compareByDescending { it.first.lastUpdated } - fun setRepositoryMenu(value: RepositoryMenu) { - viewModelScope.launch { - userPreferencesRepository.setRepositoryMenu(value) + Option.Size -> + compareByDescending { 0 } } - } - @AssistedFactory - interface Factory { - fun create(repo: Repo): RepositoryViewModel - } + return if (descending) base.reversed() else base + } - companion object { - @Composable - fun build(repo: Repo): RepositoryViewModel = - hiltViewModel { factory -> - factory.create(repo) - } + fun search(key: String) { + keyFlow.value = key + } + + fun openSearch() { + isSearch = true + } + + fun closeSearch() { + isSearch = false + keyFlow.value = "" + } + + fun setRepositoryMenu(value: RepositoryMenu) { + viewModelScope.launch { + userPreferencesRepository.setRepositoryMenu(value) } } + + @AssistedFactory + interface Factory { + fun create(repo: Repo): RepositoryViewModel + } + + companion object { + @Composable + fun build(repo: Repo): RepositoryViewModel = + androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel { factory -> + factory.create(repo) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/SuperUserViewModel.kt b/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/SuperUserViewModel.kt deleted file mode 100644 index 25dd5f0eb..000000000 --- a/app/src/main/kotlin/com/dergoogler/mmrl/viewmodel/SuperUserViewModel.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.dergoogler.mmrl.viewmodel - -import android.app.Application -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.os.Parcelable -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.Density -import androidx.core.graphics.drawable.toBitmap -import androidx.lifecycle.viewModelScope -import coil3.request.ImageRequest -import coil3.request.crossfade -import com.dergoogler.mmrl.datastore.UserPreferencesRepository -import com.dergoogler.mmrl.datastore.model.SuperUserMenu -import com.dergoogler.mmrl.platform.PlatformManager -import com.dergoogler.mmrl.platform.ksu.KsuNative -import com.dergoogler.mmrl.platform.ksu.Profile -import com.dergoogler.mmrl.repository.LocalRepository -import com.dergoogler.mmrl.repository.ModulesRepository -import com.dergoogler.mmrl.utils.packages -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import java.text.Collator -import java.util.Locale -import javax.inject.Inject - -@HiltViewModel -class SuperUserViewModel - @Inject - constructor( - application: Application, - localRepository: LocalRepository, - modulesRepository: ModulesRepository, - userPreferencesRepository: UserPreferencesRepository, - ) : MMRLViewModel(application, localRepository, modulesRepository, userPreferencesRepository) { - private val superUserMenu - get() = userPreferencesRepository.data.map { it.superUserMenu } - - var isSearch by mutableStateOf(false) - private set - private val keyFlow = MutableStateFlow("") - val query get() = keyFlow.asStateFlow() - - val listState: LazyListState = LazyListState() - - private val cacheFlow = MutableStateFlow(listOf()) - private val localFlow = MutableStateFlow(listOf()) - val local get() = localFlow.asStateFlow() - - private var isLoadingFlow = MutableStateFlow(false) - val isLoading get() = isLoadingFlow.asStateFlow() - - private val profileOverrides = mutableStateMapOf() - - val screenState: StateFlow = - localFlow - .combine(isLoadingFlow) { items, isRefreshing -> - SuperUserScreenState(items = items, isRefreshing = isRefreshing) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = SuperUserScreenState(), - ) - - init { - providerObserver() - dataObserver() - keyObserver() - } - - private fun providerObserver() { - PlatformManager.isAliveFlow - .onEach { if (it) fetchAppList() } - .launchIn(viewModelScope) - } - - private val comparator get() = - compareBy { - when { - it.profile != null && it.profile.allowSu -> 0 - it.profile != null && - ( - if (it.profile.allowSu) !it.profile.rootUseDefault else !it.profile.nonRootUseDefault - ) -> 1 - - else -> 2 - } - }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) - - private fun dataObserver() { - combine(cacheFlow, superUserMenu) { list, menu -> - if (list.isEmpty()) return@combine - - val comparator = - compareBy { - when { - it.allowSu -> 0 - it.hasCustomProfile -> 1 - else -> 2 - } - }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) - - localFlow.value = - list - .filter { - menu.showSystemApps || - (it.uid == 2000 || (it.packageInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_SYSTEM) == 0) - }.sortedWith(comparator) - }.launchIn(viewModelScope) - } - - private fun keyObserver() { - combine(keyFlow, cacheFlow, superUserMenu) { key, source, menu -> - val newKey = key.trim() - localFlow.value = - source - .filter { - if (newKey.isNotBlank()) { - it.label.contains(newKey, true) || - it.packageName.contains(newKey, true) - } else { - true - } - }.map { app -> - profileOverrides[app.packageName]?.let { app.copy(profile = it) } ?: app - }.filter { - menu.showSystemApps || - (it.uid == 2000 || (it.packageInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_SYSTEM) == 0) - }.sortedWith(comparator) - }.launchIn(viewModelScope) - } - - fun search(key: String) { - keyFlow.value = key - } - - fun openSearch() { - isSearch = true - } - - fun closeSearch() { - isSearch = false - keyFlow.value = "" - } - - fun setSuperUserMenu(value: SuperUserMenu) = - viewModelScope.launch { - userPreferencesRepository.setSuperUserMenu(value) - } - - private inline fun T.refreshing(block: T.() -> Unit) { - isLoadingFlow.update { true } - block() - isLoadingFlow.update { false } - } - - fun fetchAppList() = - viewModelScope.launch { - refreshing { - cacheFlow.value = - withContext(Dispatchers.IO) { - getAppList() - } - profileOverrides.clear() - } - } - - private fun getAppList(): List { - val pm = context.packageManager - val packages = context.packages - return packages - .map { pkg -> - val appInfo = pkg.applicationInfo!! - val uid = appInfo.uid - val profile = KsuNative.getAppProfile(pkg.packageName, uid) - AppInfo( - label = appInfo.loadLabel(pm).toString(), - packageInfo = pkg, - profile = profile, - ) - }.filter { it.packageName != context.packageName } - } - - fun updateAppProfile( - packageName: String, - newProfile: Profile, - ) { - profileOverrides[packageName] = newProfile - } - - @Parcelize - data class AppInfo( - val label: String, - val packageInfo: PackageInfo, - val profile: Profile? = null, - ) : Parcelable { - val packageName get() = packageInfo.packageName - val uid get() = packageInfo.applicationInfo!!.uid - val allowSu get() = profile?.allowSu == true - val hasCustomProfile - get() = - profile?.let { - if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault - } ?: false - - companion object { - fun AppInfo.loadIcon(context: Context): ImageRequest { - val drawable: Drawable? = - packageInfo.applicationInfo?.loadIcon(context.packageManager) - val density = Density(context) - val icon: Bitmap? = - with(density) { - drawable?.toBitmap() - } - - return ImageRequest - .Builder(context) - .data(icon) - .crossfade(true) - .build() - } - } - } - - data class SuperUserScreenState( - val items: List = emptyList(), - val isRefreshing: Boolean = false, - ) - } diff --git a/app/src/main/kotlin/dev/mmrlx/compose/ui/toolbar/NavigateUpToolbar.kt b/app/src/main/kotlin/dev/mmrlx/compose/ui/toolbar/NavigateUpToolbar.kt new file mode 100644 index 000000000..3675c15aa --- /dev/null +++ b/app/src/main/kotlin/dev/mmrlx/compose/ui/toolbar/NavigateUpToolbar.kt @@ -0,0 +1,129 @@ +package dev.mmrlx.compose.ui.toolbar + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.navigation.NavController +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import dev.mmrlx.compose.ui.icon.Icon +import dev.mmrlx.compose.ui.icon.IconButton +import dev.mmrlx.ui.R + +@Composable +fun NavigateUpToolbar( + title: String, + navController: NavController, + modifier: Modifier = Modifier, + subtitle: String? = null, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = ToolbarDefaults.windowInsets, + scrollBehavior: ToolbarScrollBehavior? = null, +) = NavigateUpToolbar( + modifier = modifier, + title = title, + subtitle = subtitle, + onBack = { navController.popBackStack() }, + actions = actions, + windowInsets = windowInsets, + scrollBehavior = scrollBehavior, + enable = enable, +) + +@Composable +fun NavigateUpToolbar( + title: String, + navigator: DestinationsNavigator, + modifier: Modifier = Modifier, + subtitle: String? = null, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = ToolbarDefaults.windowInsets, + scrollBehavior: ToolbarScrollBehavior? = null, +) = NavigateUpToolbar( + modifier = modifier, + title = title, + subtitle = subtitle, + onBack = { navigator.popBackStack() }, + actions = actions, + windowInsets = windowInsets, + scrollBehavior = scrollBehavior, + enable = enable, +) + +@Composable +fun NavigateUpToolbar( + title: String, + modifier: Modifier = Modifier, + context: Context = LocalContext.current, + subtitle: String? = null, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = ToolbarDefaults.windowInsets, + scrollBehavior: ToolbarScrollBehavior? = null, +) = NavigateUpToolbar( + modifier = modifier, + title = title, + subtitle = subtitle, + onBack = { (context as ComponentActivity).finish() }, + actions = actions, + windowInsets = windowInsets, + scrollBehavior = scrollBehavior, + enable = enable, +) + +@Composable +fun NavigateUpToolbar( + title: String, + onBack: () -> Unit, + modifier: Modifier = Modifier, + subtitle: String? = null, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = ToolbarDefaults.windowInsets, + scrollBehavior: ToolbarScrollBehavior? = null, +) = NavigateUpToolbar( + modifier = modifier, + title = { + ToolbarTitle(title = title, subtitle = subtitle) + }, + onBack = onBack, + actions = actions, + windowInsets = windowInsets, + scrollBehavior = scrollBehavior, + enable = enable, +) + +@Composable +fun NavigateUpToolbar( + title: @Composable () -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier, + enable: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, + windowInsets: WindowInsets = ToolbarDefaults.windowInsets, + scrollBehavior: ToolbarScrollBehavior? = null, +) = Toolbar( + title = title, + modifier = modifier, + navigationIcon = { + if (enable) { + IconButton( + onClick = { if (enable) onBack() }, + ) { + Icon( + painter = painterResource(id = R.drawable.arrow_left), + contentDescription = null, + ) + } + } + }, + actions = actions, + windowInsets = windowInsets, + scrollBehavior = scrollBehavior, +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91b494309..279f9b1c2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -372,8 +372,8 @@ Failed to close the action execution MMRL WebUI This module includes a MMRL only WebUI. - MMRL WebUI Config - This module uses a config file for WebUI\'s in MMRL + WebUI X Config + This module uses a configuration file for WebUI\'s in WebUI X FileSystem API This module maybe used the FileSystem API provided by MMRL. Eruda Console diff --git a/build-logic/src/main/kotlin/ProjectExt.kt b/build-logic/src/main/kotlin/ProjectExt.kt index b25057701..0ddbb72aa 100644 --- a/build-logic/src/main/kotlin/ProjectExt.kt +++ b/build-logic/src/main/kotlin/ProjectExt.kt @@ -8,7 +8,7 @@ import java.util.Properties // #### CONFIG START #### -const val COMPILE_SDK = 36 +const val COMPILE_SDK = 37 const val BUILD_TOOLS_VERSION = "$COMPILE_SDK.0.0" const val MIN_SDK = 26 diff --git a/compat/build.gradle.kts b/compat/build.gradle.kts index b9f919f45..b13c7915a 100644 --- a/compat/build.gradle.kts +++ b/compat/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.dergoogler.mmrl.compat" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 26 diff --git a/compat/settings.gradle.kts b/compat/settings.gradle.kts index 10a989ad5..c799adea2 100644 --- a/compat/settings.gradle.kts +++ b/compat/settings.gradle.kts @@ -13,6 +13,6 @@ dependencyResolutionManagement { include( ":platform", - ":ui" + ":ui", ":ext" ) diff --git a/datastore/build.gradle.kts b/datastore/build.gradle.kts index 59eaa7051..a2e294e62 100644 --- a/datastore/build.gradle.kts +++ b/datastore/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "com.dergoogler.mmrl.datastore" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 26 diff --git a/datastore/settings.gradle.kts b/datastore/settings.gradle.kts index 10a989ad5..c799adea2 100644 --- a/datastore/settings.gradle.kts +++ b/datastore/settings.gradle.kts @@ -13,6 +13,6 @@ dependencyResolutionManagement { include( ":platform", - ":ui" + ":ui", ":ext" ) diff --git a/ext/build.gradle.kts b/ext/build.gradle.kts index 72b3575cd..65f8d45f3 100644 --- a/ext/build.gradle.kts +++ b/ext/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.dergoogler.mmrl.ext" - compileSdk = 36 + compileSdk = 37 defaultConfig { minSdk = 26 diff --git a/ext/src/main/kotlin/com/dergoogler/mmrl/ext/PaddingValuesExt.kt b/ext/src/main/kotlin/com/dergoogler/mmrl/ext/PaddingValuesExt.kt index 06e3121f8..add7da461 100644 --- a/ext/src/main/kotlin/com/dergoogler/mmrl/ext/PaddingValuesExt.kt +++ b/ext/src/main/kotlin/com/dergoogler/mmrl/ext/PaddingValuesExt.kt @@ -11,7 +11,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection -inline operator fun PaddingValues.plus(other: PaddingValues): PaddingValues = OperatorPaddingValues(this, other, Dp::plus) +inline operator fun PaddingValues.plus(other: Dp): PaddingValues = OperatorPaddingValues(this, + PaddingValues(other), Dp::plus) inline operator fun PaddingValues.minus(other: PaddingValues): PaddingValues = OperatorPaddingValues(this, other, Dp::minus) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 095d5a087..0b03d2734 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,60 +1,60 @@ [versions] -androidGradlePlugin = "9.0.0" -androidxActivity = "1.10.1" -androidxAnnotation = "1.9.1" -androidxAppCompat = "1.7.1" -androidxCompose = "1.8.3" -androidxComposeMaterial3 = "1.3.2" -androidxCore = "1.16.0" -androidxCoreSplashscreen = "1.0.1" -androidxDataStore = "1.1.7" +androidGradlePlugin = "9.4.1" +androidxActivity = "1.13.0" +androidxAnnotation = "1.11.0" +androidxAppCompat = "1.8.0" +androidxCompose = "1.12.1" +androidxComposeMaterial3 = "1.4.0" +androidxCore = "1.19.1" +androidxCoreSplashscreen = "1.2.0" +androidxDataStore = "1.2.1" androidxDocumentFile = "1.1.0" -androidxHiltNavigationCompose = "1.2.0" -androidxLifecycle = "2.9.1" -androidxNavigation = "2.9.0" +androidxHiltNavigationCompose = "1.4.0" +androidxLifecycle = "2.11.0" +androidxNavigation = "2.10.2" arbor = "2.0.103" -foundation = "1.8.3" +foundation = "1.10.6" +cashLicensee = "1.13.0" kotlinxHtmlJvm = "0.9.1" multiplatformMarkdownRenderer = "0.27.0" processPhoenix = "3.0.0" -webkit = "1.14.0" +webkit = "1.17.1" multidex = "2.0.1" -navigationRuntimeKtx = "2.9.0" -androidxRoom = "2.7.2" +navigationRuntimeKtx = "2.8.5" +androidxRoom = "2.8.5" browser = "1.8.0" -coilCompose = "2.7.0" +coilCompose = "2.1.0" +coilSvg = "3.6.3" composeMarkdown = "0.5.4" hiddenApiRefine = "4.4.0" -hilt = "2.59.2" -kotlin = "2.2.0" +hilt = "2.60.1" +kotlin = "2.4.20" kotlinReflect = "1.9.24" kotlinxCoroutines = "1.8.1" kotlinxDatetime = "0.6.0" kotlinxSerialization = "1.7.1" -ksp = "2.2.0-2.0.2" +ksp = "2.3.11" libsu = "6.0.0" -protobuf = "4.34.1" -protobufPlugin = "0.9.6" +protobuf = "4.28.2" +protobufPlugin = "0.9.4" semver = "2.0.0" shizuku = "13.1.5" squareRetrofit = "2.11.0" squareOkhttp = "4.12.0" squareMoshi = "1.15.1" -runtimeAndroid = "1.7.6" -mmrlx = "1.0.152" -webuiXPortable = "c92f5f6c3c" +runtimeAndroid = "1.12.1" +mmrlx = "1.0.231" workRuntimeKtx = "2.10.0" lifecycleProcess = "2.8.7" -apacheCommons = "1.28.0" +apacheCommons = "1.27.1" parcelablelist = "2.0.1" -zoomableVersion = "2.2.0" -kotlinVersion = "2.2.0" +zoomableVersion = "2.15.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" material = "1.12.0" -material3Android = "1.3.2" -runtimeAndroidVersion = "1.7.8" +materialIcons = "1.7.8" +material3Android = "1.4.0" swiperefreshlayout = "1.1.0" composedestinations = "2.2.0" @@ -84,8 +84,9 @@ androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lif androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-material = { module = "androidx.compose.material:material" } -androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } -androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } +androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIcons" } +androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" } +androidx-multidex = { group = "androidx.multidex", name = "multidex", version.ref = "multidex" } androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navigation-runtime-ktx", version.ref = "navigationRuntimeKtx" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" } @@ -111,7 +112,7 @@ kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", lib-zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomableVersion" } libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } -libsu-io = { module = "com.github.topjohnwu.libsu:io", version.ref = "libsu" } +libsu-io = { module = "com.github.topjohnwu.libsu:io", name = "io", version.ref = "libsu" } multiplatform-markdown-renderer-android = { module = "com.mikepenz:multiplatform-markdown-renderer-android", version.ref = "multiplatformMarkdownRenderer" } multiplatform-markdown-renderer-coil3 = { module = "com.mikepenz:multiplatform-markdown-renderer-coil3", version.ref = "multiplatformMarkdownRenderer" } multiplatform-markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatformMarkdownRenderer" } @@ -131,19 +132,20 @@ square-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = square-okhttp-dnsoverhttps = { group = "com.squareup.okhttp3", name = "okhttp-dnsoverhttps", version.ref = "squareOkhttp" } square-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "squareOkhttp" } square-moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "squareMoshi" } -square-moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "squareMoshi" } - +square-moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "squareMoshi" } dev-rikka-rikkax-parcelablelist = { module = "dev.rikka.rikkax.parcelablelist:parcelablelist", version.ref = "parcelablelist" } - markwon-core = "io.noties.markwon:core:4.6.2" hiddenApiBypass = "org.lsposed.hiddenapibypass:hiddenapibypass:6.1" mmrlx-terminal = { module = "dev.mmrlx:terminal", version.ref = "mmrlx" } mmrlx-webui-core = { module = "dev.mmrlx:webui.core", version.ref = "mmrlx" } +mmrlx-nio = { module = "dev.mmrlx:nio", version.ref = "mmrlx" } +mmrlx-ui = { module = "dev.mmrlx:ui", version.ref = "mmrlx" } +mmrlx-utilities = { module = "dev.mmrlx:utilities", version.ref = "mmrlx" } timber = "com.jakewharton.timber:timber:5.0.1" # Dependencies of the included build-logic android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } -compose-gradle= { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } +compose-gradle = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } kotlin-gradle = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } ksp-gradle = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } @@ -153,10 +155,8 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" } -runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroidVersion" } +runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" } -webuix-hwui = { module = "com.github.MMRLApp.WebUI-X-Portable:hwui", version.ref = "webuiXPortable" } -webuix-helper = { module = "com.github.MMRLApp.WebUI-X-Portable:helper", version.ref = "webuiXPortable" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -176,4 +176,4 @@ self-library = { id = "self.library", version = "unspecified" } self-compose = { id = "self.compose", version = "unspecified" } self-hilt = { id = "self.hilt", version = "unspecified" } self-room = { id = "self.room", version = "unspecified" } -jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinVersion" } +jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6510bd8ba..809449e78 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ -#Sat Mar 28 13:59:40 CET 2026 +#Fri Oct 09 20:46:13 CEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/platform/build.gradle.kts b/platform/build.gradle.kts index da888f8bf..7b5860312 100644 --- a/platform/build.gradle.kts +++ b/platform/build.gradle.kts @@ -10,7 +10,7 @@ plugins { android { namespace = "com.dergoogler.mmrl.platform" - compileSdk = 36 + compileSdk = 37 publishing { singleVariant("release") diff --git a/platform/src/main/kotlin/com/dergoogler/mmrl/platform/Platform.kt b/platform/src/main/kotlin/com/dergoogler/mmrl/platform/Platform.kt index 7fd1bc033..81b243847 100644 --- a/platform/src/main/kotlin/com/dergoogler/mmrl/platform/Platform.kt +++ b/platform/src/main/kotlin/com/dergoogler/mmrl/platform/Platform.kt @@ -172,7 +172,7 @@ enum class Platform( // Category checks val isKernelSuVariant: Boolean get() = type is PlatformType.KernelSU val isKernelSuOrNext: Boolean get() = this == KernelSU || this == KsuNext - val isValid: Boolean get() = this != NonRoot + val isValid: Boolean get() = this != NonRoot && this != Unknown // Negation checks (computed properties for consistency) val isNotMagisk: Boolean get() = !isMagisk diff --git a/platform/src/main/kotlin/com/dergoogler/mmrl/platform/PlatformManager.kt b/platform/src/main/kotlin/com/dergoogler/mmrl/platform/PlatformManager.kt index 86377b170..074b99921 100644 --- a/platform/src/main/kotlin/com/dergoogler/mmrl/platform/PlatformManager.kt +++ b/platform/src/main/kotlin/com/dergoogler/mmrl/platform/PlatformManager.kt @@ -14,13 +14,6 @@ import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.dergoogler.mmrl.platform.PlatformManager.isAlive -import com.dergoogler.mmrl.platform.PlatformManager.isAliveDeferred -import com.dergoogler.mmrl.platform.PlatformManager.isAliveFlow -import com.dergoogler.mmrl.platform.PlatformManager.mService -import com.dergoogler.mmrl.platform.PlatformManager.proxyBy -import com.dergoogler.mmrl.platform.PlatformManager.serviceOrNull -import com.dergoogler.mmrl.platform.PlatformManager.state import com.dergoogler.mmrl.platform.content.IService import com.dergoogler.mmrl.platform.content.Service import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager diff --git a/settings.gradle.kts b/settings.gradle.kts index 7910b760c..58879b97a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,7 @@ dependencyResolutionManagement drm@{ repositories { google() mavenCentral() + mavenLocal() maven("https://jitpack.io") if (user != null && pass != null) { @@ -31,6 +32,7 @@ pluginManagement { repositories { google() mavenCentral() + mavenLocal() gradlePluginPortal() } } diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index a7cc481db..d89502abe 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "com.dergoogler.mmrl.ui" - compileSdk = 36 + compileSdk = 37 publishing { singleVariant("release") @@ -73,4 +73,5 @@ dependencies { implementation(libs.androidx.material3.android) implementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.ui.tooling.preview) + implementation("androidx.compose.material:material-icons-extended:1.7.8") } \ No newline at end of file diff --git a/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/BottomSheet.kt b/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/BottomSheet.kt index 17a5194dd..0e8239648 100644 --- a/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/BottomSheet.kt +++ b/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/BottomSheet.kt @@ -50,14 +50,14 @@ fun BottomSheet( modifier, sheetState, sheetMaxWidth, - shape, - containerColor, - contentColor, - tonalElevation, - scrimColor, - dragHandle, - { windowInsets }, - properties, +// shape, +// containerColor, +// contentColor, +// tonalElevation, +// scrimColor, +// dragHandle, +// { windowInsets }, +// properties, ) { val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 28.dp diff --git a/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelItem.kt b/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelItem.kt index b5f9f85c8..d83bca037 100644 --- a/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelItem.kt +++ b/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/LabelItem.kt @@ -201,6 +201,14 @@ fun MMRLLabel() { ) } +@Composable +fun WebUIXLabel() { + LabelItem( + icon = R.drawable.wxp_logo, + text = "WebUI X", + ) +} + @Composable fun SukiSU() { LabelItem( diff --git a/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/text/BBCodeText.kt b/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/text/BBCodeText.kt index 5f436f227..1d6fd5c8e 100644 --- a/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/text/BBCodeText.kt +++ b/ui/src/main/kotlin/com/dergoogler/mmrl/ui/component/text/BBCodeText.kt @@ -55,11 +55,13 @@ enum class BBCodeTag { ; companion object { - fun disableAllExcept(vararg tags: BBCodeTag): Set = entries.filterNot { it in tags }.toSet() + fun disableAllExcept(vararg tags: BBCodeTag): Set = + entries.filterNot { it in tags }.toSet() fun disableAll(): Set = entries.toSet() - fun enableAllExcept(vararg tags: BBCodeTag): Set = entries.filterNot { it in tags }.toSet() + fun enableAllExcept(vararg tags: BBCodeTag): Set = + entries.filterNot { it in tags }.toSet() } } @@ -204,7 +206,11 @@ fun BBCodeText( val ann = string - .getStringAnnotations(tag = "URL", start = position, end = position) + .getStringAnnotations( + tag = "URL", + start = position, + end = position + ) .firstOrNull() ann?.let { @@ -327,13 +333,11 @@ private fun String.toStyleMarkup( placeholderVerticalAlign = PlaceholderVerticalAlign.Center, ), ) { - runCatching { - ProvideContentColorTextStyle( - contentColor = color, - textStyle = textStyle, - ) { - iconContent(value) - } + ProvideContentColorTextStyle( + contentColor = color, + textStyle = textStyle, + ) { + iconContent(value) } } } @@ -350,9 +354,7 @@ private fun String.toStyleMarkup( placeholderVerticalAlign = PlaceholderVerticalAlign.Center, ), ) { - runCatching { - imageContent(value) - } + imageContent(value) } } @@ -360,8 +362,16 @@ private fun String.toStyleMarkup( val current = styleStack.peek() val newStyle = when (tagType) { - BBCodeTag.COLOR -> current.copy(tag = tagType, color = colorFromName(value)) - BBCodeTag.BACKGROUND -> current.copy(tag = tagType, bg = colorFromName(value)) + BBCodeTag.COLOR -> current.copy( + tag = tagType, + color = colorFromName(value) + ) + + BBCodeTag.BACKGROUND -> current.copy( + tag = tagType, + bg = colorFromName(value) + ) + BBCodeTag.BOLD -> current.copy(tag = tagType, bold = true) BBCodeTag.ITALIC -> current.copy(tag = tagType, italic = true) BBCodeTag.UNDERLINE -> current.copy(tag = tagType, underline = true) diff --git a/ui/src/main/res/drawable/wxp_logo.xml b/ui/src/main/res/drawable/wxp_logo.xml new file mode 100644 index 000000000..3720d1718 --- /dev/null +++ b/ui/src/main/res/drawable/wxp_logo.xml @@ -0,0 +1,41 @@ + + + + + + +