commit 853ceec290c698ea7ba787abfb3e196832a575e5
parent b35bbfb6b20cff5fd5631eff583ecbb13ad57b01
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Wed,  2 Aug 2023 21:44:07 +0200

refactor: packages

Diffstat:
Mapp/src/main/AndroidManifest.xml | 4++--
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt | 45---------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/Navigation.kt | 98-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/Section.kt | 77-----------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/Theme.kt | 163-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/data/InstallationSummary.kt | 17-----------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt | 36------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/sections/DebugSection.kt | 5-----
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/sections/HomeSection.kt | 109-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/sections/NotImplemented.kt | 16----------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/CallbackAlias.kt | 5-----
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/Dialogs.kt | 199-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/FeaturesSection.kt | 321-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/Requirements.kt | 33---------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/SetupActivity.kt | 137-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/SetupScreen.kt | 12------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/FfmpegScreen.kt | 18------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/LanguageScreen.kt | 13-------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/MappingsScreen.kt | 17-----------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/SaveFolderScreen.kt | 18------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/WelcomeScreen.kt | 18------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/util/SaveFolderChecker.kt | 43-------------------------------------------
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/Theme.kt | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt | 45+++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/ManagerContext.kt | 39+++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/data/InstallationSummary.kt | 17+++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/DebugSection.kt | 5+++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/NotImplemented.kt | 16++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/CallbackAlias.kt | 5+++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/util/SaveFolderChecker.kt | 43+++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt | 33+++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupContext.kt | 15+++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/SetupScreen.kt | 12++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/FfmpegScreen.kt | 18++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/LanguageScreen.kt | 12++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt | 17+++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt | 18++++++++++++++++++
Aapp/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/WelcomeScreen.kt | 18++++++++++++++++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt | 6+++---
Mcore/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 4++--
Acore/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcore/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt | 98-------------------------------------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt | 20+++++++++++++-------
Mcore/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt | 4++--
51 files changed, 1537 insertions(+), 1516 deletions(-)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml @@ -38,7 +38,7 @@ </service> <activity - android:name=".manager.MainActivity" + android:name=".ui.manager.MainActivity" android:theme="@style/AppTheme" android:exported="true"> <intent-filter> @@ -47,7 +47,7 @@ </intent-filter> </activity> <activity - android:name=".manager.setup.SetupActivity" + android:name=".ui.setup.SetupActivity" android:exported="true" android:theme="@style/AppTheme" android:excludeFromRecents="true"> diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt @@ -1,45 +0,0 @@ -package me.rhunk.snapenhance.manager - -import android.annotation.SuppressLint -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.navigation.compose.rememberNavController -import me.rhunk.snapenhance.manager.data.ManagerContext -import me.rhunk.snapenhance.manager.util.SaveFolderChecker -import me.rhunk.snapenhance.util.ActivityResultCallback - -class MainActivity : ComponentActivity() { - private val activityResultCallbacks = mutableMapOf<Int, ActivityResultCallback>() - - @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME - val managerContext = ManagerContext(this) - - //FIXME: temporary save folder - SaveFolderChecker.askForFolder( - this, - managerContext.config.root.downloader.saveFolder) - { - managerContext.config.writeConfig() - } - - setContent { - val navController = rememberNavController() - val navigation = Navigation(managerContext) - AppMaterialTheme { - Scaffold( - containerColor = MaterialTheme.colorScheme.background, - bottomBar = { navigation.NavBar(navController = navController) } - ) { innerPadding -> - navigation.NavigationHost(navController = navController, innerPadding = innerPadding, startDestination = startDestination) - } - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Navigation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Navigation.kt @@ -1,98 +0,0 @@ -package me.rhunk.snapenhance.manager - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.NavDestination.Companion.hierarchy -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.currentBackStackEntryAsState -import me.rhunk.snapenhance.manager.data.ManagerContext - - -class Navigation( - private val context: ManagerContext -) { - @Composable - fun NavigationHost( - startDestination: EnumSection, - navController: NavHostController, - innerPadding: PaddingValues - ) { - val sections = remember { EnumSection.values().toList().map { - it to it.section.constructors.first().call() - }.onEach { (section, instance) -> - instance.enumSection = section - instance.manager = context - instance.navController = navController - } } - - NavHost(navController, startDestination = startDestination.route, Modifier.padding(innerPadding)) { - sections.forEach { (_, instance) -> - instance.build(this) - } - } - } - - @Composable - fun NavBar( - navController: NavController - ) { - NavigationBar { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - EnumSection.values().toList().forEach { section -> - fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true - - NavigationBarItem( - modifier = Modifier - .requiredWidth(75.dp) - .fillMaxHeight(), - icon = { - val iconOffset by animateDpAsState( - if (selected()) 0.dp else 10.dp, - label = "" - ) - Icon( - imageVector = section.icon, - contentDescription = null, - modifier = Modifier.offset(y = iconOffset) - ) - }, - - label = { - val labelOffset by animateDpAsState( - if (selected()) 0.dp else (-5).dp, - label = "" - ) - Text(text = if (selected()) section.title else "", modifier = Modifier.offset(y = labelOffset)) - }, - selected = selected(), - onClick = { - navController.navigate(section.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Section.kt @@ -1,76 +0,0 @@ -package me.rhunk.snapenhance.manager - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Group -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.Stars -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import me.rhunk.snapenhance.manager.data.ManagerContext -import me.rhunk.snapenhance.manager.sections.HomeSection -import me.rhunk.snapenhance.manager.sections.NotImplemented -import me.rhunk.snapenhance.manager.sections.features.FeaturesSection -import kotlin.reflect.KClass - -enum class EnumSection( - val route: String, - val title: String, - val icon: ImageVector, - val section: KClass<out Section> = NotImplemented::class -) { - DOWNLOADS( - route = "downloads", - title = "Downloads", - icon = Icons.Filled.Download - ), - FEATURES( - route = "features", - title = "Features", - icon = Icons.Filled.Stars, - section = FeaturesSection::class - ), - HOME( - route = "home", - title = "Home", - icon = Icons.Filled.Home, - section = HomeSection::class - ), - FRIENDS( - route = "friends", - title = "Friends", - icon = Icons.Filled.Group - ), - DEBUG( - route = "debug", - title = "Debug", - icon = Icons.Filled.BugReport - ); - - companion object { - fun fromRoute(route: String): EnumSection { - return values().first { it.route == route } - } - } -} - - - -open class Section { - lateinit var enumSection: EnumSection - lateinit var manager: ManagerContext - lateinit var navController: NavController - - @Composable - open fun Content() { NotImplemented() } - - open fun build(navGraphBuilder: NavGraphBuilder) { - navGraphBuilder.composable(enumSection.route) { - Content() - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Theme.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/Theme.kt @@ -1,162 +0,0 @@ -package me.rhunk.snapenhance.manager - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext - -val md_theme_light_primary = Color(0xFF6750A4) -val md_theme_light_onPrimary = Color(0xFFFFFFFF) -val md_theme_light_primaryContainer = Color(0xFFE9DDFF) -val md_theme_light_onPrimaryContainer = Color(0xFF22005D) -val md_theme_light_secondary = Color(0xFF625B71) -val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFFE8DEF8) -val md_theme_light_onSecondaryContainer = Color(0xFF1E192B) -val md_theme_light_tertiary = Color(0xFF3C5BA9) -val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFDBE1FF) -val md_theme_light_onTertiaryContainer = Color(0xFF001849) -val md_theme_light_error = Color(0xFFBA1A1A) -val md_theme_light_errorContainer = Color(0xFFFFDAD6) -val md_theme_light_onError = Color(0xFFFFFFFF) -val md_theme_light_onErrorContainer = Color(0xFF410002) -val md_theme_light_background = Color(0xFFFFFBFF) -val md_theme_light_onBackground = Color(0xFF1C1B1E) -val md_theme_light_surface = Color(0xFFFFFBFF) -val md_theme_light_onSurface = Color(0xFF1C1B1E) -val md_theme_light_surfaceVariant = Color(0xFFE7E0EB) -val md_theme_light_onSurfaceVariant = Color(0xFF49454E) -val md_theme_light_outline = Color(0xFF7A757F) -val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4) -val md_theme_light_inverseSurface = Color(0xFF313033) -val md_theme_light_inversePrimary = Color(0xFFCFBCFF) -val md_theme_light_surfaceTint = Color(0xFF6750A4) -val md_theme_light_outlineVariant = Color(0xFFCAC4CF) -val md_theme_light_scrim = Color(0xFF000000) - -val md_theme_dark_primary = Color(0xFFCFBCFF) -val md_theme_dark_onPrimary = Color(0xFF381E72) -val md_theme_dark_primaryContainer = Color(0xFF4F378A) -val md_theme_dark_onPrimaryContainer = Color(0xFFE9DDFF) -val md_theme_dark_secondary = Color(0xFFCBC2DB) -val md_theme_dark_onSecondary = Color(0xFF332D41) -val md_theme_dark_secondaryContainer = Color(0xFF4A4458) -val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8) -val md_theme_dark_tertiary = Color(0xFFB3C5FF) -val md_theme_dark_onTertiary = Color(0xFF002B75) -val md_theme_dark_tertiaryContainer = Color(0xFF21428F) -val md_theme_dark_onTertiaryContainer = Color(0xFFDBE1FF) -val md_theme_dark_error = Color(0xFFFFB4AB) -val md_theme_dark_errorContainer = Color(0xFF93000A) -val md_theme_dark_onError = Color(0xFF690005) -val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -val md_theme_dark_background = Color(0xFF1C1B1E) -val md_theme_dark_onBackground = Color(0xFFE6E1E6) -val md_theme_dark_surface = Color(0xFF1C1B1E) -val md_theme_dark_onSurface = Color(0xFFE6E1E6) -val md_theme_dark_surfaceVariant = Color(0xFF49454E) -val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4CF) -val md_theme_dark_outline = Color(0xFF948F99) -val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1E) -val md_theme_dark_inverseSurface = Color(0xFFE6E1E6) -val md_theme_dark_inversePrimary = Color(0xFF6750A4) -val md_theme_dark_surfaceTint = Color(0xFFCFBCFF) -val md_theme_dark_outlineVariant = Color(0xFF49454E) -val md_theme_dark_scrim = Color(0xFF000000) - - -val seed = Color(0xFF6750A4) - - -private val LightThemeColors = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - onError = md_theme_light_onError, - errorContainer = md_theme_light_errorContainer, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim -) - -private val DarkThemeColors = lightColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - onError = md_theme_dark_onError, - errorContainer = md_theme_dark_errorContainer, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim -) - -@Composable -fun AppMaterialTheme( - isDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit -) { - val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - val colorScheme = when { - dynamicColor && isDarkTheme -> { - dynamicDarkColorScheme(LocalContext.current) - } - dynamicColor && !isDarkTheme -> { - dynamicLightColorScheme(LocalContext.current) - } - !isDarkTheme -> LightThemeColors - else -> DarkThemeColors - } - - MaterialTheme( - colorScheme = colorScheme, - content = content - ) -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/InstallationSummary.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/InstallationSummary.kt @@ -1,17 +0,0 @@ -package me.rhunk.snapenhance.manager.data - - -data class SnapchatAppInfo( - val version: String, - val versionCode: Long -) - -data class ModMappingsInfo( - val generatedSnapchatVersion: Long, - val isOutdated: Boolean -) - -data class InstallationSummary( - val snapchatInfo: SnapchatAppInfo?, - val mappingsInfo: ModMappingsInfo? -) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/data/ManagerContext.kt @@ -1,35 +0,0 @@ -package me.rhunk.snapenhance.manager.data - -import android.content.Context -import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper -import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper -import me.rhunk.snapenhance.core.config.ModConfig - -class ManagerContext( - private val context: Context -) { - val config = ModConfig() - val translation = TranslationWrapper() - val mappings = MappingsWrapper(context) - - init { - config.loadFromContext(context) - translation.loadFromContext(context) - mappings.apply { loadFromContext(context) }.init() - } - - fun getInstallationSummary() = InstallationSummary( - snapchatInfo = mappings.getSnapchatPackageInfo()?.let { - SnapchatAppInfo( - version = it.versionName, - versionCode = it.longVersionCode - ) - }, - mappingsInfo = if (mappings.isMappingsLoaded()) { - ModMappingsInfo( - generatedSnapchatVersion = mappings.getGeneratedBuildNumber(), - isOutdated = mappings.isMappingsOutdated() - ) - } else null - ) -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/DebugSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/DebugSection.kt @@ -1,4 +0,0 @@ -package me.rhunk.snapenhance.manager.sections - -class DebugSection { -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/HomeSection.kt @@ -1,108 +0,0 @@ -package me.rhunk.snapenhance.manager.sections - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Map -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import me.rhunk.snapenhance.manager.Section -import me.rhunk.snapenhance.manager.data.InstallationSummary - -class HomeSection : Section() { - companion object { - val cardMargin = 10.dp - } - - @OptIn(ExperimentalLayoutApi::class) - @Composable - private fun SummaryCards(installationSummary: InstallationSummary) { - //installation summary - OutlinedCard( - modifier = Modifier - .padding(all = cardMargin) - .fillMaxWidth() - ) { - Column(modifier = Modifier.padding(all = 16.dp)) { - if (installationSummary.snapchatInfo != null) { - Text("Snapchat version: ${installationSummary.snapchatInfo.version}") - Text("Snapchat version code: ${installationSummary.snapchatInfo.versionCode}") - } else { - Text("Snapchat not installed/detected") - } - } - } - - OutlinedCard( - modifier = Modifier - .padding(all = cardMargin) - .fillMaxWidth() - ) { - FlowRow( - modifier = Modifier.padding(all = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Icon( - Icons.Filled.Map, - contentDescription = "Mappings", - modifier = Modifier - .padding(end = 10.dp) - .align(Alignment.CenterVertically) - ) - - Text(text = if (installationSummary.mappingsInfo == null || installationSummary.mappingsInfo.isOutdated) { - "Mappings ${if (installationSummary.mappingsInfo == null) "not generated" else "outdated"}" - } else { - "Mappings version ${installationSummary.mappingsInfo.generatedSnapchatVersion}" - }, modifier = Modifier.weight(1f) - .align(Alignment.CenterVertically) - ) - - //inline button - Button(onClick = {}, modifier = Modifier.height(40.dp)) { - Icon(Icons.Filled.Refresh, contentDescription = "Refresh") - } - } - } - } - - @Composable - @Preview - override fun Content() { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(ScrollState(0)) - ) { - Text( - "SnapEnhance", - fontSize = 32.sp, - modifier = Modifier.padding(32.dp) - ) - - Text( - text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl.", - modifier = Modifier.padding(16.dp) - ) - - SummaryCards(manager.getInstallationSummary()) - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/NotImplemented.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/NotImplemented.kt @@ -1,15 +0,0 @@ -package me.rhunk.snapenhance.manager.sections - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import me.rhunk.snapenhance.manager.Section - -class NotImplemented : Section() { - @Composable - override fun Content() { - Column { - Text(text = "Not implemented yet!") - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/CallbackAlias.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/CallbackAlias.kt @@ -1,4 +0,0 @@ -package me.rhunk.snapenhance.manager.sections.features - -typealias ClickCallback = (Boolean) -> Unit -typealias RegisterClickCallback = (ClickCallback) -> ClickCallback- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/Dialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/Dialogs.kt @@ -1,199 +0,0 @@ -package me.rhunk.snapenhance.manager.sections.features - -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import me.rhunk.snapenhance.core.config.DataProcessors -import me.rhunk.snapenhance.core.config.PropertyPair - - -class Dialogs { - @Composable - fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) { - Card( - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .padding(10.dp, 5.dp, 10.dp, 10.dp), - ) { - Column( - modifier = Modifier - .padding(10.dp, 10.dp, 10.dp, 10.dp) - .verticalScroll(ScrollState(0)), - ) { content() } - } - } - - @Composable - fun DefaultEntryText(text: String, modifier: Modifier = Modifier) { - Text( - text = text, - modifier = Modifier - .padding(10.dp, 10.dp, 10.dp, 10.dp) - .then(modifier) - ) - } - - @Composable - @Suppress("UNCHECKED_CAST") - fun UniqueSelectionDialog(property: PropertyPair<*>) { - val keys = (property.value.defaultValues as List<String>).toMutableList().apply { - add(0, "disabled") - } - - val selectedValue = remember { - mutableStateOf(property.value.getNullable()?.toString() ?: "disabled") - } - - DefaultDialogCard { - keys.forEachIndexed { index, item -> - fun select() { - selectedValue.value = item - property.value.setAny(if (index == 0) { - null - } else { - item - }) - } - - Row( - modifier = Modifier.clickable { select() }, - verticalAlignment = Alignment.CenterVertically - ) { - DefaultEntryText( - text = item, - modifier = Modifier.weight(1f) - ) - RadioButton( - selected = selectedValue.value == item, - onClick = { select() } - ) - } - } - } - } - - @Composable - fun KeyboardInputDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) { - val focusRequester = remember { FocusRequester() } - - DefaultDialogCard { - val fieldValue = remember { - mutableStateOf(property.value.get().toString().let { - TextFieldValue( - text = it, - selection = TextRange(it.length) - ) - }) - } - - TextField( - modifier = Modifier - .fillMaxWidth() - .padding(all = 10.dp) - .onGloballyPositioned { - focusRequester.requestFocus() - } - .focusRequester(focusRequester), - value = fieldValue.value, - onValueChange = { - fieldValue.value = it - }, - keyboardOptions = when (property.key.dataType.type) { - DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number) - DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal) - else -> KeyboardOptions(keyboardType = KeyboardType.Text) - }, - singleLine = true - ) - - Row( - modifier = Modifier.padding(top = 10.dp).fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Button(onClick = { dismiss() }) { - Text(text = "Cancel") - } - Button(onClick = { - if (property.key.dataType.type == DataProcessors.Type.INTEGER) { - runCatching { - property.value.setAny(fieldValue.value.text.toInt()) - }.onFailure { - property.value.setAny(0) - } - } else { - property.value.setAny(fieldValue.value.text) - } - dismiss() - }) { - Text(text = "Ok") - } - } - } - } - - @Composable - @Suppress("UNCHECKED_CAST") - fun MultipleSelectionDialog(property: PropertyPair<*>) { - val defaultItems = property.value.defaultValues as List<String> - val toggledStates = property.value.get() as MutableList<String> - DefaultDialogCard { - defaultItems.forEach { key -> - val state = remember { - mutableStateOf(toggledStates.contains(key)) - } - - fun toggle(value: Boolean? = null) { - state.value = value ?: !state.value - if (state.value) { - toggledStates.add(key) - } else { - toggledStates.remove(key) - } - } - - Row( - modifier = Modifier.clickable { toggle() }, - verticalAlignment = Alignment.CenterVertically - ) { - DefaultEntryText( - text = key, - modifier = Modifier - .weight(1f) - ) - Switch( - checked = state.value, - onCheckedChange = { - toggle(it) - } - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/sections/features/FeaturesSection.kt @@ -1,320 +0,0 @@ -package me.rhunk.snapenhance.manager.sections.features - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.OpenInNew -import androidx.compose.material.icons.rounded.Save -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledIconButton -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.navigation -import kotlinx.coroutines.launch -import me.rhunk.snapenhance.core.config.ConfigContainer -import me.rhunk.snapenhance.core.config.DataProcessors -import me.rhunk.snapenhance.core.config.PropertyPair -import me.rhunk.snapenhance.manager.Section - -class FeaturesSection : Section() { - private val dialogs by lazy { Dialogs() } - - companion object { - private const val MAIN_ROUTE = "root" - } - - @Composable - private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) { - val showDialog = remember { mutableStateOf(false) } - val dialogComposable = remember { mutableStateOf<@Composable () -> Unit>({}) } - - fun registerDialogOnClickCallback() = registerClickCallback { - showDialog.value = true - } - - if (showDialog.value) { - Dialog( - onDismissRequest = { showDialog.value = false }, - properties = DialogProperties() - ) { - dialogComposable.value() - } - } - - val propertyValue = property.value - - when (val dataType = remember { property.key.dataType.type }) { - DataProcessors.Type.BOOLEAN -> { - val state = remember { mutableStateOf(propertyValue.get() as Boolean) } - Switch( - checked = state.value, - onCheckedChange = registerClickCallback { - state.value = state.value.not() - propertyValue.setAny(state.value) - } - ) - } - - DataProcessors.Type.STRING_UNIQUE_SELECTION -> { - registerDialogOnClickCallback() - - dialogComposable.value = { - dialogs.UniqueSelectionDialog(property) - } - - Text( - overflow = TextOverflow.Ellipsis, - maxLines = 1, - modifier = Modifier.widthIn(0.dp, 120.dp), - text = (propertyValue.getNullable() as? String) ?: "Disabled", - ) - } - - DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { - dialogComposable.value = { - when (dataType) { - DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { - dialogs.MultipleSelectionDialog(property) - } - DataProcessors.Type.STRING, DataProcessors.Type.INTEGER -> { - dialogs.KeyboardInputDialog(property) { showDialog.value = false } - } - else -> {} - } - } - - registerDialogOnClickCallback().let { { it.invoke(true) } }.also { - if (dataType == DataProcessors.Type.INTEGER || dataType == DataProcessors.Type.FLOAT) { - FilledIconButton(onClick = it) { - Text( - text = propertyValue.get().toString(), - modifier = Modifier.wrapContentWidth(), - overflow = TextOverflow.Ellipsis - ) - } - } else { - IconButton(onClick = it) { - Icon(Icons.Filled.OpenInNew, contentDescription = null) - } - } - } - } - DataProcessors.Type.CONTAINER -> { - val container = propertyValue.get() as ConfigContainer - - registerClickCallback { - navController.navigate("container/${property.name}") - } - - if (container.globalState == null) return - - val state = remember { mutableStateOf(container.globalState!!) } - - Box( - modifier = Modifier - .padding(end = 15.dp), - ) { - - Box(modifier = Modifier - .height(50.dp) - .width(1.dp) - .background(color = MaterialTheme.colors.onBackground.copy(alpha = 0.12f), shape = RoundedCornerShape(5.dp))) - } - - Switch( - checked = state.value, - onCheckedChange = { - state.value = state.value.not() - container.globalState = state.value - } - ) - } - } - - } - - @Composable - private fun PropertyCard(property: PropertyPair<*>) { - val clickCallback = remember { mutableStateOf<ClickCallback?>(null) } - Card( - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, end = 10.dp, top = 5.dp, bottom = 5.dp) - ) { - Row( - modifier = Modifier - .fillMaxSize() - .clickable { - clickCallback.value?.invoke(true) - } - .padding(all = 4.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f, fill = true) - .padding(all = 10.dp) - ) { - Text( - text = property.name, - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - Text( - text = property.name, - fontSize = 12.sp, - lineHeight = 15.sp - ) - } - - Row( - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(all = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - PropertyAction(property, registerClickCallback = { callback -> - clickCallback.value = callback - callback - }) - } - } - } - } - - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - private fun Container( - containerName: String, - configContainer: ConfigContainer - ) { - val properties = remember { - configContainer.properties.map { PropertyPair(it.key, it.value) } - } - - val scope = rememberCoroutineScope() - val scaffoldState = rememberBottomSheetScaffoldState() - Scaffold( - snackbarHost = { SnackbarHost(scaffoldState.snackbarHostState) }, - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - title = { - Text(text = containerName, textAlign = TextAlign.Center) - }, - navigationIcon = { - if (navController.currentBackStackEntry?.destination?.route != MAIN_ROUTE) { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Filled.ArrowBack, contentDescription = null) - } - } - } - ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { - manager.config.writeConfig() - scope.launch { - scaffoldState.snackbarHostState.showSnackbar("Saved") - } - }, - modifier = Modifier.padding(25.dp), - containerColor = MaterialTheme.colors.primary, - contentColor = MaterialTheme.colors.onPrimary, - shape = RoundedCornerShape(16.dp), - ) { - Icon( - imageVector = Icons.Rounded.Save, - contentDescription = null - ) - } - }, - content = { innerPadding -> - LazyColumn( - modifier = Modifier - .fillMaxHeight() - .padding(innerPadding), - verticalArrangement = Arrangement.Center - ) { - items(properties) { - PropertyCard(it) - } - } - } - ) - - } - - override fun build(navGraphBuilder: NavGraphBuilder) { - val allContainers by lazy { - val containers = mutableMapOf<String, ConfigContainer>() - fun queryContainerRecursive(container: ConfigContainer) { - container.properties.forEach { - if (it.key.dataType.type == DataProcessors.Type.CONTAINER) { - containers[it.key.name] = it.value.get() as ConfigContainer - queryContainerRecursive(it.value.get() as ConfigContainer) - } - } - } - queryContainerRecursive(manager.config.root) - containers - } - - navGraphBuilder.navigation(route = "features", startDestination = MAIN_ROUTE) { - composable(MAIN_ROUTE) { - Container(MAIN_ROUTE, manager.config.root) - } - - composable("container/{name}") { backStackEntry -> - backStackEntry.arguments?.getString("name")?.let { containerName -> - allContainers[containerName]?.let { - Container(containerName, it) - } - } - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/Requirements.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/Requirements.kt @@ -1,33 +0,0 @@ -package me.rhunk.snapenhance.manager.setup - -import android.os.Bundle - -data class Requirements( - val firstRun: Boolean = false, - val language: Boolean = false, - val mappings: Boolean = false, - val saveFolder: Boolean = false, - val ffmpeg: Boolean = false -) { - companion object { - fun fromBundle(bundle: Bundle): Requirements { - return Requirements( - firstRun = bundle.getBoolean("firstRun"), - language = bundle.getBoolean("language"), - mappings = bundle.getBoolean("mappings"), - saveFolder = bundle.getBoolean("saveFolder"), - ffmpeg = bundle.getBoolean("ffmpeg") - ) - } - - fun toBundle(requirements: Requirements): Bundle { - return Bundle().apply { - putBoolean("firstRun", requirements.firstRun) - putBoolean("language", requirements.language) - putBoolean("mappings", requirements.mappings) - putBoolean("saveFolder", requirements.saveFolder) - putBoolean("ffmpeg", requirements.ffmpeg) - } - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/SetupActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/SetupActivity.kt @@ -1,136 +0,0 @@ -package me.rhunk.snapenhance.manager.setup - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowForwardIos -import androidx.compose.material3.FilledIconButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.unit.dp -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import me.rhunk.snapenhance.manager.AppMaterialTheme -import me.rhunk.snapenhance.manager.setup.screens.SetupScreen -import me.rhunk.snapenhance.manager.setup.screens.impl.FfmpegScreen -import me.rhunk.snapenhance.manager.setup.screens.impl.LanguageScreen -import me.rhunk.snapenhance.manager.setup.screens.impl.MappingsScreen -import me.rhunk.snapenhance.manager.setup.screens.impl.SaveFolderScreen -import me.rhunk.snapenhance.manager.setup.screens.impl.WelcomeScreen - - -class SetupActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val requirements = intent.getBundleExtra("requirements")?.let { - Requirements.fromBundle(it) - } ?: Requirements(firstRun = true) - - val requiredScreens = mutableListOf<SetupScreen>() - - with(requiredScreens) { - with(requirements) { - if (firstRun || language) add(LanguageScreen().apply { route = "language" }) - if (firstRun) add(WelcomeScreen().apply { route = "welcome" }) - if (firstRun || saveFolder) add(SaveFolderScreen().apply { route = "saveFolder" }) - if (firstRun || mappings) add(MappingsScreen().apply { route = "mappings" }) - if (firstRun || ffmpeg) add(FfmpegScreen().apply { route = "ffmpeg" }) - } - } - - if (requiredScreens.isEmpty()) { - finish() - return - } - - setContent { - val navController = rememberNavController() - val canGoNext = remember { mutableStateOf(false) } - - fun nextScreen() { - if (!canGoNext.value) return - canGoNext.value = false - if (requiredScreens.size > 1) { - requiredScreens.removeFirst() - navController.navigate(requiredScreens.first().route) - } else { - finish() - } - } - - AppMaterialTheme { - Scaffold( - containerColor = MaterialTheme.colorScheme.background, - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val alpha: Float by animateFloatAsState(if (canGoNext.value) 1f else 0f, - label = "NextButton" - ) - - FilledIconButton( - onClick = { nextScreen() }, - modifier = Modifier.padding(50.dp) - .width(60.dp) - .height(60.dp) - .alpha(alpha) - ) { - Icon( - imageVector = Icons.Default.ArrowForwardIos, - contentDescription = null - ) - } - } - }, - ) { paddingValues -> - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - .padding(paddingValues) - ) { - NavHost( - navController = navController, - startDestination = requiredScreens.first().route - ) { - requiredScreens.forEach { screen -> - screen.allowNext = { canGoNext.value = it } - composable(screen.route) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - screen.Content() - } - } - } - } - } - } - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/SetupScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/SetupScreen.kt @@ -1,11 +0,0 @@ -package me.rhunk.snapenhance.manager.setup.screens - -import androidx.compose.runtime.Composable - -abstract class SetupScreen { - lateinit var allowNext: (Boolean) -> Unit - lateinit var route: String - - @Composable - abstract fun Content() -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/FfmpegScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/FfmpegScreen.kt @@ -1,17 +0,0 @@ -package me.rhunk.snapenhance.manager.setup.screens.impl - -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import me.rhunk.snapenhance.manager.setup.screens.SetupScreen - -class FfmpegScreen : SetupScreen() { - - @Composable - override fun Content() { - Text(text = "FFmpeg") - Button(onClick = { allowNext(true) }) { - Text(text = "Next") - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/LanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/LanguageScreen.kt @@ -1,12 +0,0 @@ -package me.rhunk.snapenhance.manager.setup.screens.impl - -import androidx.compose.runtime.Composable -import me.rhunk.snapenhance.manager.setup.screens.SetupScreen - -class LanguageScreen : SetupScreen(){ - - @Composable - override fun Content() { - - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/MappingsScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/MappingsScreen.kt @@ -1,16 +0,0 @@ -package me.rhunk.snapenhance.manager.setup.screens.impl - -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import me.rhunk.snapenhance.manager.setup.screens.SetupScreen - -class MappingsScreen : SetupScreen() { - @Composable - override fun Content() { - Text(text = "Mappings") - Button(onClick = { allowNext(true) }) { - Text(text = "Next") - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/SaveFolderScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/SaveFolderScreen.kt @@ -1,17 +0,0 @@ -package me.rhunk.snapenhance.manager.setup.screens.impl - -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import me.rhunk.snapenhance.manager.setup.screens.SetupScreen - -class SaveFolderScreen : SetupScreen() { - - @Composable - override fun Content() { - Text(text = "SaveFolder") - Button(onClick = {allowNext(true)}) { - Text(text = "Next") - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/WelcomeScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/setup/screens/impl/WelcomeScreen.kt @@ -1,17 +0,0 @@ -package me.rhunk.snapenhance.manager.setup.screens.impl - -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import me.rhunk.snapenhance.manager.setup.screens.SetupScreen - -class WelcomeScreen : SetupScreen() { - - @Composable - override fun Content() { - Text(text = "Welcome") - Button(onClick = { allowNext(true) }) { - Text(text = "Next") - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/util/SaveFolderChecker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/util/SaveFolderChecker.kt @@ -1,42 +0,0 @@ -package me.rhunk.snapenhance.manager.util - -import android.app.Activity -import android.app.AlertDialog -import android.content.Intent -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.result.contract.ActivityResultContracts -import me.rhunk.snapenhance.core.config.PropertyValue -import kotlin.system.exitProcess - -object SaveFolderChecker { - fun askForFolder(activity: ComponentActivity, property: PropertyValue<String>, saveConfig: () -> Unit) { - if (property.get().isEmpty() || !property.get().startsWith("content://")) { - val startActivity = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) result@{ - if (it.resultCode != Activity.RESULT_OK) return@result - val uri = it.data?.data ?: return@result - val value = uri.toString() - activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - property.set(value) - saveConfig() - Toast.makeText(activity, "save folder set!", Toast.LENGTH_SHORT).show() - activity.finish() - } - - AlertDialog.Builder(activity) - .setTitle("Save folder") - .setMessage("Please select a folder where you want to save downloaded files.") - .setPositiveButton("Select") { _, _ -> - startActivity.launch( - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - ) - } - .setNegativeButton("Cancel") { _, _ -> - exitProcess(0) - } - .show() - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/Theme.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/Theme.kt @@ -0,0 +1,162 @@ +package me.rhunk.snapenhance.ui + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +val md_theme_light_primary = Color(0xFF6750A4) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFE9DDFF) +val md_theme_light_onPrimaryContainer = Color(0xFF22005D) +val md_theme_light_secondary = Color(0xFF625B71) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFE8DEF8) +val md_theme_light_onSecondaryContainer = Color(0xFF1E192B) +val md_theme_light_tertiary = Color(0xFF3C5BA9) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFDBE1FF) +val md_theme_light_onTertiaryContainer = Color(0xFF001849) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFFFBFF) +val md_theme_light_onBackground = Color(0xFF1C1B1E) +val md_theme_light_surface = Color(0xFFFFFBFF) +val md_theme_light_onSurface = Color(0xFF1C1B1E) +val md_theme_light_surfaceVariant = Color(0xFFE7E0EB) +val md_theme_light_onSurfaceVariant = Color(0xFF49454E) +val md_theme_light_outline = Color(0xFF7A757F) +val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4) +val md_theme_light_inverseSurface = Color(0xFF313033) +val md_theme_light_inversePrimary = Color(0xFFCFBCFF) +val md_theme_light_surfaceTint = Color(0xFF6750A4) +val md_theme_light_outlineVariant = Color(0xFFCAC4CF) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFFCFBCFF) +val md_theme_dark_onPrimary = Color(0xFF381E72) +val md_theme_dark_primaryContainer = Color(0xFF4F378A) +val md_theme_dark_onPrimaryContainer = Color(0xFFE9DDFF) +val md_theme_dark_secondary = Color(0xFFCBC2DB) +val md_theme_dark_onSecondary = Color(0xFF332D41) +val md_theme_dark_secondaryContainer = Color(0xFF4A4458) +val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8) +val md_theme_dark_tertiary = Color(0xFFB3C5FF) +val md_theme_dark_onTertiary = Color(0xFF002B75) +val md_theme_dark_tertiaryContainer = Color(0xFF21428F) +val md_theme_dark_onTertiaryContainer = Color(0xFFDBE1FF) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF1C1B1E) +val md_theme_dark_onBackground = Color(0xFFE6E1E6) +val md_theme_dark_surface = Color(0xFF1C1B1E) +val md_theme_dark_onSurface = Color(0xFFE6E1E6) +val md_theme_dark_surfaceVariant = Color(0xFF49454E) +val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4CF) +val md_theme_dark_outline = Color(0xFF948F99) +val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1E) +val md_theme_dark_inverseSurface = Color(0xFFE6E1E6) +val md_theme_dark_inversePrimary = Color(0xFF6750A4) +val md_theme_dark_surfaceTint = Color(0xFFCFBCFF) +val md_theme_dark_outlineVariant = Color(0xFF49454E) +val md_theme_dark_scrim = Color(0xFF000000) + + +val seed = Color(0xFF6750A4) + + +private val LightThemeColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + onError = md_theme_light_onError, + errorContainer = md_theme_light_errorContainer, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim +) + +private val DarkThemeColors = lightColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + onError = md_theme_dark_onError, + errorContainer = md_theme_dark_errorContainer, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim +) + +@Composable +fun AppMaterialTheme( + isDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + val colorScheme = when { + dynamicColor && isDarkTheme -> { + dynamicDarkColorScheme(LocalContext.current) + } + dynamicColor && !isDarkTheme -> { + dynamicLightColorScheme(LocalContext.current) + } + !isDarkTheme -> LightThemeColors + else -> DarkThemeColors + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt @@ -0,0 +1,45 @@ +package me.rhunk.snapenhance.ui.manager + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.navigation.compose.rememberNavController +import me.rhunk.snapenhance.ui.AppMaterialTheme +import me.rhunk.snapenhance.ui.manager.util.SaveFolderChecker +import me.rhunk.snapenhance.util.ActivityResultCallback + +class MainActivity : ComponentActivity() { + private val activityResultCallbacks = mutableMapOf<Int, ActivityResultCallback>() + + @SuppressLint("UnusedMaterialScaffoldPaddingParameter") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME + val managerContext = ManagerContext(this) + + //FIXME: temporary save folder + SaveFolderChecker.askForFolder( + this, + managerContext.config.root.downloader.saveFolder) + { + managerContext.config.writeConfig() + } + + setContent { + val navController = rememberNavController() + val navigation = Navigation(managerContext) + AppMaterialTheme { + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + bottomBar = { navigation.NavBar(navController = navController) } + ) { innerPadding -> + navigation.NavigationHost(navController = navController, innerPadding = innerPadding, startDestination = startDestination) + } + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/ManagerContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/ManagerContext.kt @@ -0,0 +1,38 @@ +package me.rhunk.snapenhance.ui.manager + +import android.content.Context +import me.rhunk.snapenhance.bridge.wrapper.MappingsWrapper +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper +import me.rhunk.snapenhance.core.config.ModConfig +import me.rhunk.snapenhance.ui.manager.data.InstallationSummary +import me.rhunk.snapenhance.ui.manager.data.ModMappingsInfo +import me.rhunk.snapenhance.ui.manager.data.SnapchatAppInfo + +class ManagerContext( + private val context: Context +) { + val config = ModConfig() + val translation = LocaleWrapper() + val mappings = MappingsWrapper(context) + + init { + config.loadFromContext(context) + translation.loadFromContext(context) + mappings.apply { loadFromContext(context) }.init() + } + + fun getInstallationSummary() = InstallationSummary( + snapchatInfo = mappings.getSnapchatPackageInfo()?.let { + SnapchatAppInfo( + version = it.versionName, + versionCode = it.longVersionCode + ) + }, + mappingsInfo = if (mappings.isMappingsLoaded()) { + ModMappingsInfo( + generatedSnapchatVersion = mappings.getGeneratedBuildNumber(), + isOutdated = mappings.isMappingsOutdated() + ) + } else null + ) +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt @@ -0,0 +1,97 @@ +package me.rhunk.snapenhance.ui.manager + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.currentBackStackEntryAsState + + +class Navigation( + private val context: ManagerContext +) { + @Composable + fun NavigationHost( + startDestination: EnumSection, + navController: NavHostController, + innerPadding: PaddingValues + ) { + val sections = remember { EnumSection.values().toList().map { + it to it.section.constructors.first().call() + }.onEach { (section, instance) -> + instance.enumSection = section + instance.manager = context + instance.navController = navController + } } + + NavHost(navController, startDestination = startDestination.route, Modifier.padding(innerPadding)) { + sections.forEach { (_, instance) -> + instance.build(this) + } + } + } + + @Composable + fun NavBar( + navController: NavController + ) { + NavigationBar { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + EnumSection.values().toList().forEach { section -> + fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true + + NavigationBarItem( + modifier = Modifier + .requiredWidth(75.dp) + .fillMaxHeight(), + icon = { + val iconOffset by animateDpAsState( + if (selected()) 0.dp else 10.dp, + label = "" + ) + Icon( + imageVector = section.icon, + contentDescription = null, + modifier = Modifier.offset(y = iconOffset) + ) + }, + + label = { + val labelOffset by animateDpAsState( + if (selected()) 0.dp else (-5).dp, + label = "" + ) + Text(text = if (selected()) section.title else "", modifier = Modifier.offset(y = labelOffset)) + }, + selected = selected(), + onClick = { + navController.navigate(section.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -0,0 +1,75 @@ +package me.rhunk.snapenhance.ui.manager + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Group +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Stars +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import me.rhunk.snapenhance.ui.manager.sections.HomeSection +import me.rhunk.snapenhance.ui.manager.sections.NotImplemented +import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection +import kotlin.reflect.KClass + +enum class EnumSection( + val route: String, + val title: String, + val icon: ImageVector, + val section: KClass<out Section> = NotImplemented::class +) { + DOWNLOADS( + route = "downloads", + title = "Downloads", + icon = Icons.Filled.Download + ), + FEATURES( + route = "features", + title = "Features", + icon = Icons.Filled.Stars, + section = FeaturesSection::class + ), + HOME( + route = "home", + title = "Home", + icon = Icons.Filled.Home, + section = HomeSection::class + ), + FRIENDS( + route = "friends", + title = "Friends", + icon = Icons.Filled.Group + ), + DEBUG( + route = "debug", + title = "Debug", + icon = Icons.Filled.BugReport + ); + + companion object { + fun fromRoute(route: String): EnumSection { + return values().first { it.route == route } + } + } +} + + + +open class Section { + lateinit var enumSection: EnumSection + lateinit var manager: ManagerContext + lateinit var navController: NavController + + @Composable + open fun Content() { NotImplemented() } + + open fun build(navGraphBuilder: NavGraphBuilder) { + navGraphBuilder.composable(enumSection.route) { + Content() + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/data/InstallationSummary.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/data/InstallationSummary.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.ui.manager.data + + +data class SnapchatAppInfo( + val version: String, + val versionCode: Long +) + +data class ModMappingsInfo( + val generatedSnapchatVersion: Long, + val isOutdated: Boolean +) + +data class InstallationSummary( + val snapchatInfo: SnapchatAppInfo?, + val mappingsInfo: ModMappingsInfo? +) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/DebugSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/DebugSection.kt @@ -0,0 +1,4 @@ +package me.rhunk.snapenhance.ui.manager.sections + +class DebugSection { +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/HomeSection.kt @@ -0,0 +1,108 @@ +package me.rhunk.snapenhance.ui.manager.sections + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Map +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.rhunk.snapenhance.ui.manager.Section +import me.rhunk.snapenhance.ui.manager.data.InstallationSummary + +class HomeSection : Section() { + companion object { + val cardMargin = 10.dp + } + + @OptIn(ExperimentalLayoutApi::class) + @Composable + private fun SummaryCards(installationSummary: InstallationSummary) { + //installation summary + OutlinedCard( + modifier = Modifier + .padding(all = cardMargin) + .fillMaxWidth() + ) { + Column(modifier = Modifier.padding(all = 16.dp)) { + if (installationSummary.snapchatInfo != null) { + Text("Snapchat version: ${installationSummary.snapchatInfo.version}") + Text("Snapchat version code: ${installationSummary.snapchatInfo.versionCode}") + } else { + Text("Snapchat not installed/detected") + } + } + } + + OutlinedCard( + modifier = Modifier + .padding(all = cardMargin) + .fillMaxWidth() + ) { + FlowRow( + modifier = Modifier.padding(all = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Icon( + Icons.Filled.Map, + contentDescription = "Mappings", + modifier = Modifier + .padding(end = 10.dp) + .align(Alignment.CenterVertically) + ) + + Text(text = if (installationSummary.mappingsInfo == null || installationSummary.mappingsInfo.isOutdated) { + "Mappings ${if (installationSummary.mappingsInfo == null) "not generated" else "outdated"}" + } else { + "Mappings version ${installationSummary.mappingsInfo.generatedSnapchatVersion}" + }, modifier = Modifier.weight(1f) + .align(Alignment.CenterVertically) + ) + + //inline button + Button(onClick = {}, modifier = Modifier.height(40.dp)) { + Icon(Icons.Filled.Refresh, contentDescription = "Refresh") + } + } + } + } + + @Composable + @Preview + override fun Content() { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(ScrollState(0)) + ) { + Text( + "SnapEnhance", + fontSize = 32.sp, + modifier = Modifier.padding(32.dp) + ) + + Text( + text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl. Donec euismod, nisl eget ultricies ultrices, nunc nisl aliquam nunc, quis aliquam nisl nunc eu nisl.", + modifier = Modifier.padding(16.dp) + ) + + SummaryCards(manager.getInstallationSummary()) + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/NotImplemented.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/NotImplemented.kt @@ -0,0 +1,15 @@ +package me.rhunk.snapenhance.ui.manager.sections + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import me.rhunk.snapenhance.ui.manager.Section + +class NotImplemented : Section() { + @Composable + override fun Content() { + Column { + Text(text = "Not implemented yet!") + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/CallbackAlias.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/CallbackAlias.kt @@ -0,0 +1,4 @@ +package me.rhunk.snapenhance.ui.manager.sections.features + +typealias ClickCallback = (Boolean) -> Unit +typealias RegisterClickCallback = (ClickCallback) -> ClickCallback+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/Dialogs.kt @@ -0,0 +1,199 @@ +package me.rhunk.snapenhance.ui.manager.sections.features + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import me.rhunk.snapenhance.core.config.DataProcessors +import me.rhunk.snapenhance.core.config.PropertyPair + + +class Dialogs { + @Composable + fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) { + Card( + shape = MaterialTheme.shapes.medium, + modifier = Modifier + .padding(10.dp, 5.dp, 10.dp, 10.dp), + ) { + Column( + modifier = Modifier + .padding(10.dp, 10.dp, 10.dp, 10.dp) + .verticalScroll(ScrollState(0)), + ) { content() } + } + } + + @Composable + fun DefaultEntryText(text: String, modifier: Modifier = Modifier) { + Text( + text = text, + modifier = Modifier + .padding(10.dp, 10.dp, 10.dp, 10.dp) + .then(modifier) + ) + } + + @Composable + @Suppress("UNCHECKED_CAST") + fun UniqueSelectionDialog(property: PropertyPair<*>) { + val keys = (property.value.defaultValues as List<String>).toMutableList().apply { + add(0, "disabled") + } + + val selectedValue = remember { + mutableStateOf(property.value.getNullable()?.toString() ?: "disabled") + } + + DefaultDialogCard { + keys.forEachIndexed { index, item -> + fun select() { + selectedValue.value = item + property.value.setAny(if (index == 0) { + null + } else { + item + }) + } + + Row( + modifier = Modifier.clickable { select() }, + verticalAlignment = Alignment.CenterVertically + ) { + DefaultEntryText( + text = item, + modifier = Modifier.weight(1f) + ) + RadioButton( + selected = selectedValue.value == item, + onClick = { select() } + ) + } + } + } + } + + @Composable + fun KeyboardInputDialog(property: PropertyPair<*>, dismiss: () -> Unit = {}) { + val focusRequester = remember { FocusRequester() } + + DefaultDialogCard { + val fieldValue = remember { + mutableStateOf(property.value.get().toString().let { + TextFieldValue( + text = it, + selection = TextRange(it.length) + ) + }) + } + + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp) + .onGloballyPositioned { + focusRequester.requestFocus() + } + .focusRequester(focusRequester), + value = fieldValue.value, + onValueChange = { + fieldValue.value = it + }, + keyboardOptions = when (property.key.dataType.type) { + DataProcessors.Type.INTEGER -> KeyboardOptions(keyboardType = KeyboardType.Number) + DataProcessors.Type.FLOAT -> KeyboardOptions(keyboardType = KeyboardType.Decimal) + else -> KeyboardOptions(keyboardType = KeyboardType.Text) + }, + singleLine = true + ) + + Row( + modifier = Modifier.padding(top = 10.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Button(onClick = { dismiss() }) { + Text(text = "Cancel") + } + Button(onClick = { + if (property.key.dataType.type == DataProcessors.Type.INTEGER) { + runCatching { + property.value.setAny(fieldValue.value.text.toInt()) + }.onFailure { + property.value.setAny(0) + } + } else { + property.value.setAny(fieldValue.value.text) + } + dismiss() + }) { + Text(text = "Ok") + } + } + } + } + + @Composable + @Suppress("UNCHECKED_CAST") + fun MultipleSelectionDialog(property: PropertyPair<*>) { + val defaultItems = property.value.defaultValues as List<String> + val toggledStates = property.value.get() as MutableList<String> + DefaultDialogCard { + defaultItems.forEach { key -> + val state = remember { + mutableStateOf(toggledStates.contains(key)) + } + + fun toggle(value: Boolean? = null) { + state.value = value ?: !state.value + if (state.value) { + toggledStates.add(key) + } else { + toggledStates.remove(key) + } + } + + Row( + modifier = Modifier.clickable { toggle() }, + verticalAlignment = Alignment.CenterVertically + ) { + DefaultEntryText( + text = key, + modifier = Modifier + .weight(1f) + ) + Switch( + checked = state.value, + onCheckedChange = { + toggle(it) + } + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt @@ -0,0 +1,320 @@ +package me.rhunk.snapenhance.ui.manager.sections.features + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.OpenInNew +import androidx.compose.material.icons.rounded.Save +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import kotlinx.coroutines.launch +import me.rhunk.snapenhance.core.config.ConfigContainer +import me.rhunk.snapenhance.core.config.DataProcessors +import me.rhunk.snapenhance.core.config.PropertyPair +import me.rhunk.snapenhance.ui.manager.Section + +class FeaturesSection : Section() { + private val dialogs by lazy { Dialogs() } + + companion object { + private const val MAIN_ROUTE = "root" + } + + @Composable + private fun PropertyAction(property: PropertyPair<*>, registerClickCallback: RegisterClickCallback) { + val showDialog = remember { mutableStateOf(false) } + val dialogComposable = remember { mutableStateOf<@Composable () -> Unit>({}) } + + fun registerDialogOnClickCallback() = registerClickCallback { + showDialog.value = true + } + + if (showDialog.value) { + Dialog( + onDismissRequest = { showDialog.value = false }, + properties = DialogProperties() + ) { + dialogComposable.value() + } + } + + val propertyValue = property.value + + when (val dataType = remember { property.key.dataType.type }) { + DataProcessors.Type.BOOLEAN -> { + val state = remember { mutableStateOf(propertyValue.get() as Boolean) } + Switch( + checked = state.value, + onCheckedChange = registerClickCallback { + state.value = state.value.not() + propertyValue.setAny(state.value) + } + ) + } + + DataProcessors.Type.STRING_UNIQUE_SELECTION -> { + registerDialogOnClickCallback() + + dialogComposable.value = { + dialogs.UniqueSelectionDialog(property) + } + + Text( + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier.widthIn(0.dp, 120.dp), + text = (propertyValue.getNullable() as? String) ?: "Disabled", + ) + } + + DataProcessors.Type.STRING_MULTIPLE_SELECTION, DataProcessors.Type.STRING, DataProcessors.Type.INTEGER, DataProcessors.Type.FLOAT -> { + dialogComposable.value = { + when (dataType) { + DataProcessors.Type.STRING_MULTIPLE_SELECTION -> { + dialogs.MultipleSelectionDialog(property) + } + DataProcessors.Type.STRING, DataProcessors.Type.INTEGER -> { + dialogs.KeyboardInputDialog(property) { showDialog.value = false } + } + else -> {} + } + } + + registerDialogOnClickCallback().let { { it.invoke(true) } }.also { + if (dataType == DataProcessors.Type.INTEGER || dataType == DataProcessors.Type.FLOAT) { + FilledIconButton(onClick = it) { + Text( + text = propertyValue.get().toString(), + modifier = Modifier.wrapContentWidth(), + overflow = TextOverflow.Ellipsis + ) + } + } else { + IconButton(onClick = it) { + Icon(Icons.Filled.OpenInNew, contentDescription = null) + } + } + } + } + DataProcessors.Type.CONTAINER -> { + val container = propertyValue.get() as ConfigContainer + + registerClickCallback { + navController.navigate("container/${property.name}") + } + + if (container.globalState == null) return + + val state = remember { mutableStateOf(container.globalState!!) } + + Box( + modifier = Modifier + .padding(end = 15.dp), + ) { + + Box(modifier = Modifier + .height(50.dp) + .width(1.dp) + .background(color = MaterialTheme.colors.onBackground.copy(alpha = 0.12f), shape = RoundedCornerShape(5.dp))) + } + + Switch( + checked = state.value, + onCheckedChange = { + state.value = state.value.not() + container.globalState = state.value + } + ) + } + } + + } + + @Composable + private fun PropertyCard(property: PropertyPair<*>) { + val clickCallback = remember { mutableStateOf<ClickCallback?>(null) } + Card( + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 5.dp, bottom = 5.dp) + ) { + Row( + modifier = Modifier + .fillMaxSize() + .clickable { + clickCallback.value?.invoke(true) + } + .padding(all = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .weight(1f, fill = true) + .padding(all = 10.dp) + ) { + Text( + text = property.name, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + Text( + text = property.name, + fontSize = 12.sp, + lineHeight = 15.sp + ) + } + + Row( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(all = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PropertyAction(property, registerClickCallback = { callback -> + clickCallback.value = callback + callback + }) + } + } + } + } + + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun Container( + containerName: String, + configContainer: ConfigContainer + ) { + val properties = remember { + configContainer.properties.map { PropertyPair(it.key, it.value) } + } + + val scope = rememberCoroutineScope() + val scaffoldState = rememberBottomSheetScaffoldState() + Scaffold( + snackbarHost = { SnackbarHost(scaffoldState.snackbarHostState) }, + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text(text = containerName, textAlign = TextAlign.Center) + }, + navigationIcon = { + if (navController.currentBackStackEntry?.destination?.route != MAIN_ROUTE) { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.Filled.ArrowBack, contentDescription = null) + } + } + } + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + manager.config.writeConfig() + scope.launch { + scaffoldState.snackbarHostState.showSnackbar("Saved") + } + }, + modifier = Modifier.padding(25.dp), + containerColor = MaterialTheme.colors.primary, + contentColor = MaterialTheme.colors.onPrimary, + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Rounded.Save, + contentDescription = null + ) + } + }, + content = { innerPadding -> + LazyColumn( + modifier = Modifier + .fillMaxHeight() + .padding(innerPadding), + verticalArrangement = Arrangement.Center + ) { + items(properties) { + PropertyCard(it) + } + } + } + ) + + } + + override fun build(navGraphBuilder: NavGraphBuilder) { + val allContainers by lazy { + val containers = mutableMapOf<String, ConfigContainer>() + fun queryContainerRecursive(container: ConfigContainer) { + container.properties.forEach { + if (it.key.dataType.type == DataProcessors.Type.CONTAINER) { + containers[it.key.name] = it.value.get() as ConfigContainer + queryContainerRecursive(it.value.get() as ConfigContainer) + } + } + } + queryContainerRecursive(manager.config.root) + containers + } + + navGraphBuilder.navigation(route = "features", startDestination = MAIN_ROUTE) { + composable(MAIN_ROUTE) { + Container(MAIN_ROUTE, manager.config.root) + } + + composable("container/{name}") { backStackEntry -> + backStackEntry.arguments?.getString("name")?.let { containerName -> + allContainers[containerName]?.let { + Container(containerName, it) + } + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/util/SaveFolderChecker.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/util/SaveFolderChecker.kt @@ -0,0 +1,42 @@ +package me.rhunk.snapenhance.ui.manager.util + +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import me.rhunk.snapenhance.core.config.PropertyValue +import kotlin.system.exitProcess + +object SaveFolderChecker { + fun askForFolder(activity: ComponentActivity, property: PropertyValue<String>, saveConfig: () -> Unit) { + if (property.get().isEmpty() || !property.get().startsWith("content://")) { + val startActivity = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) result@{ + if (it.resultCode != Activity.RESULT_OK) return@result + val uri = it.data?.data ?: return@result + val value = uri.toString() + activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + property.set(value) + saveConfig() + Toast.makeText(activity, "save folder set!", Toast.LENGTH_SHORT).show() + activity.finish() + } + + AlertDialog.Builder(activity) + .setTitle("Save folder") + .setMessage("Please select a folder where you want to save downloaded files.") + .setPositiveButton("Select") { _, _ -> + startActivity.launch( + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + ) + } + .setNegativeButton("Cancel") { _, _ -> + exitProcess(0) + } + .show() + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/Requirements.kt @@ -0,0 +1,33 @@ +package me.rhunk.snapenhance.ui.setup + +import android.os.Bundle + +data class Requirements( + val firstRun: Boolean = false, + val language: Boolean = false, + val mappings: Boolean = false, + val saveFolder: Boolean = false, + val ffmpeg: Boolean = false +) { + companion object { + fun fromBundle(bundle: Bundle): Requirements { + return Requirements( + firstRun = bundle.getBoolean("firstRun"), + language = bundle.getBoolean("language"), + mappings = bundle.getBoolean("mappings"), + saveFolder = bundle.getBoolean("saveFolder"), + ffmpeg = bundle.getBoolean("ffmpeg") + ) + } + + fun toBundle(requirements: Requirements): Bundle { + return Bundle().apply { + putBoolean("firstRun", requirements.firstRun) + putBoolean("language", requirements.language) + putBoolean("mappings", requirements.mappings) + putBoolean("saveFolder", requirements.saveFolder) + putBoolean("ffmpeg", requirements.ffmpeg) + } + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupActivity.kt @@ -0,0 +1,136 @@ +package me.rhunk.snapenhance.ui.setup + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowForwardIos +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import me.rhunk.snapenhance.ui.AppMaterialTheme +import me.rhunk.snapenhance.ui.setup.screens.SetupScreen +import me.rhunk.snapenhance.ui.setup.screens.impl.FfmpegScreen +import me.rhunk.snapenhance.ui.setup.screens.impl.LanguageScreen +import me.rhunk.snapenhance.ui.setup.screens.impl.MappingsScreen +import me.rhunk.snapenhance.ui.setup.screens.impl.SaveFolderScreen +import me.rhunk.snapenhance.ui.setup.screens.impl.WelcomeScreen + + +class SetupActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val requirements = intent.getBundleExtra("requirements")?.let { + Requirements.fromBundle(it) + } ?: Requirements(firstRun = true) + + val requiredScreens = mutableListOf<SetupScreen>() + + with(requiredScreens) { + with(requirements) { + if (firstRun || language) add(LanguageScreen().apply { route = "language" }) + if (firstRun) add(WelcomeScreen().apply { route = "welcome" }) + if (firstRun || saveFolder) add(SaveFolderScreen().apply { route = "saveFolder" }) + if (firstRun || mappings) add(MappingsScreen().apply { route = "mappings" }) + if (firstRun || ffmpeg) add(FfmpegScreen().apply { route = "ffmpeg" }) + } + } + + if (requiredScreens.isEmpty()) { + finish() + return + } + + setContent { + val navController = rememberNavController() + val canGoNext = remember { mutableStateOf(false) } + + fun nextScreen() { + if (!canGoNext.value) return + canGoNext.value = false + if (requiredScreens.size > 1) { + requiredScreens.removeFirst() + navController.navigate(requiredScreens.first().route) + } else { + finish() + } + } + + AppMaterialTheme { + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val alpha: Float by animateFloatAsState(if (canGoNext.value) 1f else 0f, + label = "NextButton" + ) + + FilledIconButton( + onClick = { nextScreen() }, + modifier = Modifier.padding(50.dp) + .width(60.dp) + .height(60.dp) + .alpha(alpha) + ) { + Icon( + imageVector = Icons.Default.ArrowForwardIos, + contentDescription = null + ) + } + } + }, + ) { paddingValues -> + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + .padding(paddingValues) + ) { + NavHost( + navController = navController, + startDestination = requiredScreens.first().route + ) { + requiredScreens.forEach { screen -> + screen.allowNext = { canGoNext.value = it } + composable(screen.route) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + screen.Content() + } + } + } + } + } + } + } + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/SetupContext.kt @@ -0,0 +1,14 @@ +package me.rhunk.snapenhance.ui.setup + +import android.content.Context +import me.rhunk.snapenhance.core.config.ModConfig + +class SetupContext( + private val context: Context +) { + val config = ModConfig() + + init { + config.loadFromContext(context) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/SetupScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/SetupScreen.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.ui.setup.screens + +import androidx.compose.runtime.Composable + +abstract class SetupScreen { + lateinit var allowNext: (Boolean) -> Unit + lateinit var route: String + + @Composable + abstract fun Content() +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/FfmpegScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/FfmpegScreen.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.ui.setup.screens.impl + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import me.rhunk.snapenhance.ui.setup.screens.SetupScreen + +class FfmpegScreen : SetupScreen() { + + @Composable + override fun Content() { + Text(text = "FFmpeg") + Button(onClick = { allowNext(true) }) { + Text(text = "Next") + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/LanguageScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/LanguageScreen.kt @@ -0,0 +1,11 @@ +package me.rhunk.snapenhance.ui.setup.screens.impl + +import androidx.compose.runtime.Composable +import me.rhunk.snapenhance.ui.setup.screens.SetupScreen + +class LanguageScreen : SetupScreen(){ + @Composable + override fun Content() { + + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/MappingsScreen.kt @@ -0,0 +1,16 @@ +package me.rhunk.snapenhance.ui.setup.screens.impl + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import me.rhunk.snapenhance.ui.setup.screens.SetupScreen + +class MappingsScreen : SetupScreen() { + @Composable + override fun Content() { + Text(text = "Mappings") + Button(onClick = { allowNext(true) }) { + Text(text = "Next") + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/SaveFolderScreen.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.ui.setup.screens.impl + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import me.rhunk.snapenhance.ui.setup.screens.SetupScreen + +class SaveFolderScreen : SetupScreen() { + + @Composable + override fun Content() { + Text(text = "SaveFolder") + Button(onClick = {allowNext(true)}) { + Text(text = "Next") + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/WelcomeScreen.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/setup/screens/impl/WelcomeScreen.kt @@ -0,0 +1,17 @@ +package me.rhunk.snapenhance.ui.setup.screens.impl + +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import me.rhunk.snapenhance.ui.setup.screens.SetupScreen + +class WelcomeScreen : SetupScreen() { + + @Composable + override fun Content() { + Text(text = "Welcome") + Button(onClick = { allowNext(true) }) { + Text(text = "Next") + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -12,7 +12,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import kotlinx.coroutines.asCoroutineDispatcher import me.rhunk.snapenhance.bridge.BridgeClient -import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.config.ModConfig import me.rhunk.snapenhance.core.eventbus.EventBus import me.rhunk.snapenhance.data.MessageSender @@ -45,7 +45,7 @@ class ModContext { val event = EventBus(this) val eventDispatcher = EventDispatcher(this) - val translation = TranslationWrapper() + val translation = LocaleWrapper() val features = FeatureManager(this) val mappings = MappingManager(this) val actionManager = ActionManager(this) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt @@ -7,7 +7,7 @@ import android.content.Intent import android.os.Build import android.os.Environment import android.provider.Settings -import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.download.DownloadTaskManager import kotlin.system.exitProcess @@ -16,7 +16,7 @@ import kotlin.system.exitProcess */ object SharedContext { lateinit var downloadTaskManager: DownloadTaskManager - lateinit var translation: TranslationWrapper + lateinit var translation: LocaleWrapper private fun askForStoragePermission(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -70,7 +70,7 @@ object SharedContext { } } if (!this::translation.isInitialized) { - translation = TranslationWrapper().apply { + translation = LocaleWrapper().apply { loadFromContext(context) } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -6,7 +6,7 @@ import android.os.IBinder import me.rhunk.snapenhance.SharedContext import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.bridge.wrapper.MessageLoggerWrapper -import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.download.DownloadProcessor class BridgeService : Service() { @@ -85,7 +85,7 @@ class BridgeService : Service() { override fun clearMessageLogger() = messageLoggerWrapper.clearMessages() - override fun fetchTranslations() = TranslationWrapper.fetchLocales(context = this@BridgeService).associate { + override fun fetchTranslations() = LocaleWrapper.fetchLocales(context = this@BridgeService).associate { it.locale to it.content } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/LocaleWrapper.kt @@ -0,0 +1,97 @@ +package me.rhunk.snapenhance.bridge.wrapper + +import android.content.Context +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.bridge.BridgeClient +import me.rhunk.snapenhance.data.LocalePair +import java.util.Locale + + +class LocaleWrapper { + companion object { + const val DEFAULT_LOCALE = "en_US" + + fun fetchLocales(context: Context): List<LocalePair> { + val deviceLocale = Locale.getDefault().toString() + val locales = mutableListOf<LocalePair>() + + locales.add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() })) + + if (deviceLocale == DEFAULT_LOCALE) return locales + + val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(deviceLocale) }?.substring(0, 5) ?: return locales + + context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream -> + locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() })) + } + + return locales + } + } + + + private val translationMap = linkedMapOf<String, String>() + private lateinit var _locale: String + + val locale by lazy { + Locale(_locale.substring(0, 2), _locale.substring(3, 5)) + } + + private fun load(localePair: LocalePair) { + if (!::_locale.isInitialized) { + _locale = localePair.locale + } + + val translations = JsonParser.parseString(localePair.content).asJsonObject + if (translations == null || translations.isJsonNull) { + return + } + + fun scanObject(jsonObject: JsonObject, prefix: String = "") { + jsonObject.entrySet().forEach { + if (it.value.isJsonPrimitive) { + val key = "$prefix${it.key}" + translationMap[key] = it.value.asString + } + if (!it.value.isJsonObject) return@forEach + scanObject(it.value.asJsonObject, "$prefix${it.key}.") + } + } + + scanObject(translations) + } + + fun loadFromBridge(bridgeClient: BridgeClient) { + bridgeClient.fetchTranslations().forEach { + load(it) + } + } + + fun loadFromContext(context: Context) { + fetchLocales(context).forEach { + load(it) + } + } + + operator fun get(key: String): String { + return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") } + } + + fun format(key: String, vararg args: Pair<String, String>): String { + return args.fold(get(key)) { acc, pair -> + acc.replace("{${pair.first}}", pair.second) + } + } + + fun getCategory(key: String): LocaleWrapper { + return LocaleWrapper().apply { + translationMap.putAll( + this@LocaleWrapper.translationMap + .filterKeys { it.startsWith("$key.") } + .mapKeys { it.key.substring(key.length + 1) } + ) + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt @@ -1,97 +0,0 @@ -package me.rhunk.snapenhance.bridge.wrapper - -import android.content.Context -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.bridge.BridgeClient -import me.rhunk.snapenhance.data.LocalePair -import java.util.Locale - - -class TranslationWrapper { - companion object { - private const val DEFAULT_LOCALE = "en_US" - - fun fetchLocales(context: Context): List<LocalePair> { - val deviceLocale = Locale.getDefault().toString() - val locales = mutableListOf<LocalePair>() - - locales.add(LocalePair(DEFAULT_LOCALE, context.resources.assets.open("lang/$DEFAULT_LOCALE.json").bufferedReader().use { it.readText() })) - - if (deviceLocale == DEFAULT_LOCALE) return locales - - val compatibleLocale = context.resources.assets.list("lang")?.firstOrNull { it.startsWith(deviceLocale) }?.substring(0, 5) ?: return locales - - context.resources.assets.open("lang/$compatibleLocale.json").use { inputStream -> - locales.add(LocalePair(compatibleLocale, inputStream.bufferedReader().use { it.readText() })) - } - - return locales - } - } - - - private val translationMap = linkedMapOf<String, String>() - private lateinit var _locale: String - - val locale by lazy { - Locale(_locale.substring(0, 2), _locale.substring(3, 5)) - } - - private fun load(localePair: LocalePair) { - if (!::_locale.isInitialized) { - _locale = localePair.locale - } - - val translations = JsonParser.parseString(localePair.content).asJsonObject - if (translations == null || translations.isJsonNull) { - return - } - - fun scanObject(jsonObject: JsonObject, prefix: String = "") { - jsonObject.entrySet().forEach { - if (it.value.isJsonPrimitive) { - val key = "$prefix${it.key}" - translationMap[key] = it.value.asString - } - if (!it.value.isJsonObject) return@forEach - scanObject(it.value.asJsonObject, "$prefix${it.key}.") - } - } - - scanObject(translations) - } - - fun loadFromBridge(bridgeClient: BridgeClient) { - bridgeClient.fetchTranslations().forEach { - load(it) - } - } - - fun loadFromContext(context: Context) { - fetchLocales(context).forEach { - load(it) - } - } - - operator fun get(key: String): String { - return translationMap[key] ?: key.also { Logger.debug("Missing translation for $key") } - } - - fun format(key: String, vararg args: Pair<String, String>): String { - return args.fold(get(key)) { acc, pair -> - acc.replace("{${pair.first}}", pair.second) - } - } - - fun getCategory(key: String): TranslationWrapper { - return TranslationWrapper().apply { - translationMap.putAll( - this@TranslationWrapper.translationMap - .filterKeys { it.startsWith("$key.") } - .mapKeys { it.key.substring(key.length + 1) } - ) - } - } -}- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/ModConfig.kt @@ -8,17 +8,21 @@ import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.bridge.BridgeClient import me.rhunk.snapenhance.bridge.FileLoaderWrapper import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.config.impl.RootConfig -class ModConfig() { - val root = RootConfig() +class ModConfig { - companion object { - val gson: Gson = GsonBuilder().setPrettyPrinting().create() - } + var locale: String = LocaleWrapper.DEFAULT_LOCALE + set(value) { + field = value + writeConfig() + } + private val gson: Gson = GsonBuilder().setPrettyPrinting().create() private val file = FileLoaderWrapper(BridgeFileType.CONFIG, "{}".toByteArray(Charsets.UTF_8)) + val root = RootConfig() operator fun getValue(thisRef: Any?, property: Any?) = root private fun load() { @@ -36,12 +40,14 @@ class ModConfig() { } private fun loadConfig() { - val configContent = file.read() - root.fromJson(gson.fromJson(configContent.toString(Charsets.UTF_8), JsonObject::class.java)) + val configFileContent = file.read() + val configObject = gson.fromJson(configFileContent.toString(Charsets.UTF_8), JsonObject::class.java) + locale = configObject.get("language")?.asString ?: LocaleWrapper.DEFAULT_LOCALE } fun writeConfig() { val configObject = root.toJson() + configObject.addProperty("language", locale) file.write(configObject.toString().toByteArray(Charsets.UTF_8)) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt @@ -16,13 +16,13 @@ import android.widget.TextView import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.BuildConfig import me.rhunk.snapenhance.core.R import me.rhunk.snapenhance.download.data.PendingDownload class DownloadManagerActivity : Activity() { - lateinit var translation: TranslationWrapper + lateinit var translation: LocaleWrapper private val backCallbacks = mutableListOf<() -> Unit>() private val fetchedDownloadTasks = mutableListOf<PendingDownload>()