commit acc92ce907b903a011bcb32e026265629304b88f
parent ba848dfd9d3ddd0a7218312639d33a34efee73a7
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Fri,  6 Jun 2025 14:19:51 +0200

refactor: remove theming feature
Deprecated after SC v13.7.0.42

Signed-off-by: rhunk <101876869+rhunk@users.noreply.github.com>

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/RemoteFileHandleManager.kt | 7-------
Mapp/src/main/kotlin/me/rhunk/snapenhance/action/EnumQuickActions.kt | 4----
Dapp/src/main/kotlin/me/rhunk/snapenhance/storage/Theming.kt | 126-------------------------------------------------------------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt | 10+++-------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/EditThemeSection.kt | 394-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/ThemeCatalog.kt | 278-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/ThemingRoot.kt | 463-------------------------------------------------------------------------------
Mcommon/src/main/assets/lang/en_US.json | 61-------------------------------------------------------------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt | 5+++--
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt | 6------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt | 1-
Dcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomTheming.kt | 131-------------------------------------------------------------------------------
12 files changed, 6 insertions(+), 1480 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteFileHandleManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteFileHandleManager.kt @@ -8,7 +8,6 @@ import me.rhunk.snapenhance.common.bridge.InternalFileHandleType import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.util.ktx.toParcelFileDescriptor -import me.rhunk.snapenhance.storage.getEnabledThemesContent import java.io.File import java.io.OutputStream @@ -115,12 +114,6 @@ class RemoteFileHandleManager( "composer/${name.substringAfterLast("/")}" ) } - FileHandleScope.THEME -> { - return ByteArrayFileHandle( - context, - context.gson.toJson(context.database.getEnabledThemesContent()).toByteArray(Charsets.UTF_8) - ) - } else -> return null } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/EnumQuickActions.kt b/app/src/main/kotlin/me/rhunk/snapenhance/action/EnumQuickActions.kt @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.action import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.History -import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.PersonSearch import androidx.compose.ui.graphics.vector.ImageVector import me.rhunk.snapenhance.ui.manager.Routes @@ -22,7 +21,4 @@ enum class EnumQuickActions( LOGGER_HISTORY("logger_history", Icons.Default.History, { loggerHistory.navigateReset() }), - THEMING("theming", Icons.Default.Palette, { - theming.navigateReset() - }) } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/Theming.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/Theming.kt @@ -1,126 +0,0 @@ -package me.rhunk.snapenhance.storage - -import android.content.ContentValues -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.runBlocking -import me.rhunk.snapenhance.common.data.DatabaseTheme -import me.rhunk.snapenhance.common.data.DatabaseThemeContent -import me.rhunk.snapenhance.common.util.ktx.getIntOrNull -import me.rhunk.snapenhance.common.util.ktx.getStringOrNull - - -fun AppDatabase.getThemeList(): List<DatabaseTheme> { - return runBlocking(executor.asCoroutineDispatcher()) { - database.rawQuery("SELECT * FROM themes ORDER BY id DESC", null).use { cursor -> - val themes = mutableListOf<DatabaseTheme>() - while (cursor.moveToNext()) { - themes.add( - DatabaseTheme( - id = cursor.getIntOrNull("id") ?: continue, - enabled = cursor.getIntOrNull("enabled") == 1, - name = cursor.getStringOrNull("name") ?: continue, - description = cursor.getStringOrNull("description"), - version = cursor.getStringOrNull("version"), - author = cursor.getStringOrNull("author"), - updateUrl = cursor.getStringOrNull("updateUrl") - ) - ) - } - themes - } - } -} - -fun AppDatabase.getThemeInfo(id: Int): DatabaseTheme? { - return runBlocking(executor.asCoroutineDispatcher()) { - database.rawQuery("SELECT * FROM themes WHERE id = ?", arrayOf(id.toString())).use { cursor -> - if (!cursor.moveToFirst()) return@use null - DatabaseTheme( - id = cursor.getIntOrNull("id") ?: return@use null, - enabled = cursor.getIntOrNull("enabled") == 1, - name = cursor.getStringOrNull("name") ?: return@use null, - description = cursor.getStringOrNull("description"), - version = cursor.getStringOrNull("version"), - author = cursor.getStringOrNull("author"), - updateUrl = cursor.getStringOrNull("updateUrl") - ) - } - } -} - -fun AppDatabase.getThemeIdByUpdateUrl(updateUrl: String): Int? { - return runBlocking(executor.asCoroutineDispatcher()) { - database.rawQuery("SELECT id FROM themes WHERE updateUrl = ?", arrayOf(updateUrl)).use { cursor -> - if (!cursor.moveToFirst()) return@use null - cursor.getIntOrNull("id") - } - } -} - -fun AppDatabase.addOrUpdateTheme(theme: DatabaseTheme, themeId: Int? = null): Int { - return runBlocking(executor.asCoroutineDispatcher()) { - val contentValues = ContentValues().apply { - put("enabled", if (theme.enabled) 1 else 0) - put("name", theme.name) - put("description", theme.description) - put("version", theme.version) - put("author", theme.author) - put("updateUrl", theme.updateUrl) - } - if (themeId != null) { - database.update("themes", contentValues, "id = ?", arrayOf(themeId.toString())) - return@runBlocking themeId - } - database.insert("themes", null, contentValues).toInt() - } -} - -fun AppDatabase.setThemeState(id: Int, enabled: Boolean) { - runBlocking(executor.asCoroutineDispatcher()) { - database.update("themes", ContentValues().apply { - put("enabled", if (enabled) 1 else 0) - }, "id = ?", arrayOf(id.toString())) - } -} - -fun AppDatabase.deleteTheme(id: Int) { - runBlocking(executor.asCoroutineDispatcher()) { - database.delete("themes", "id = ?", arrayOf(id.toString())) - } -} - - -fun AppDatabase.getThemeContent(id: Int): DatabaseThemeContent? { - return runBlocking(executor.asCoroutineDispatcher()) { - database.rawQuery("SELECT content FROM themes WHERE id = ?", arrayOf(id.toString())).use { cursor -> - if (!cursor.moveToFirst()) return@use null - runCatching { - context.gson.fromJson(cursor.getStringOrNull("content"), DatabaseThemeContent::class.java) - }.getOrNull() - } - } -} - - -fun AppDatabase.getEnabledThemesContent(): List<DatabaseThemeContent> { - return runBlocking(executor.asCoroutineDispatcher()) { - database.rawQuery("SELECT content FROM themes WHERE enabled = 1", null).use { cursor -> - val themes = mutableListOf<DatabaseThemeContent>() - while (cursor.moveToNext()) { - runCatching { - themes.add(context.gson.fromJson(cursor.getStringOrNull("content"), DatabaseThemeContent::class.java)) - } - } - themes - } - } -} - - -fun AppDatabase.setThemeContent(id: Int, content: DatabaseThemeContent) { - runBlocking(executor.asCoroutineDispatcher()) { - database.update("themes", ContentValues().apply { - put("content", context.gson.toJson(content)) - }, "id = ?", arrayOf(id.toString())) - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt @@ -11,23 +11,21 @@ import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraphBuilder import me.rhunk.snapenhance.RemoteSideContext -import me.rhunk.snapenhance.ui.manager.pages.location.BetterLocationRoot import me.rhunk.snapenhance.ui.manager.pages.FileImportsRoot import me.rhunk.snapenhance.ui.manager.pages.LoggerHistoryRoot +import me.rhunk.snapenhance.ui.manager.pages.ManageReposSection import me.rhunk.snapenhance.ui.manager.pages.TasksRootSection import me.rhunk.snapenhance.ui.manager.pages.features.FeaturesRootSection +import me.rhunk.snapenhance.ui.manager.pages.features.ManageRuleFeature import me.rhunk.snapenhance.ui.manager.pages.home.HomeLogs import me.rhunk.snapenhance.ui.manager.pages.home.HomeRootSection import me.rhunk.snapenhance.ui.manager.pages.home.HomeSettings +import me.rhunk.snapenhance.ui.manager.pages.location.BetterLocationRoot import me.rhunk.snapenhance.ui.manager.pages.scripting.ScriptingRootSection import me.rhunk.snapenhance.ui.manager.pages.social.LoggedStories import me.rhunk.snapenhance.ui.manager.pages.social.ManageScope import me.rhunk.snapenhance.ui.manager.pages.social.MessagingPreview import me.rhunk.snapenhance.ui.manager.pages.social.SocialRootSection -import me.rhunk.snapenhance.ui.manager.pages.theming.EditThemeSection -import me.rhunk.snapenhance.ui.manager.pages.ManageReposSection -import me.rhunk.snapenhance.ui.manager.pages.features.ManageRuleFeature -import me.rhunk.snapenhance.ui.manager.pages.theming.ThemingRoot import me.rhunk.snapenhance.ui.manager.pages.tracker.EditRule import me.rhunk.snapenhance.ui.manager.pages.tracker.FriendTrackerManagerRoot @@ -63,8 +61,6 @@ class Routes( val editRule = route(RouteInfo("edit_rule/?rule_id={rule_id}"), EditRule()) val fileImports = route(RouteInfo("file_imports"), FileImportsRoot()).parent(home) - val theming = route(RouteInfo("theming"), ThemingRoot()).parent(home) - val editTheme = route(RouteInfo("edit_theme/?theme_id={theme_id}"), EditThemeSection()) val manageRepos = route(RouteInfo("manage_repos"), ManageReposSection()) val social = route(RouteInfo("social", icon = Icons.Default.Group, primary = true), SocialRootSection()) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/EditThemeSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/EditThemeSection.kt @@ -1,393 +0,0 @@ -package me.rhunk.snapenhance.ui.manager.pages.theming - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -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.graphics.Color -import androidx.compose.ui.graphics.toArgb -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.navigation.NavBackStackEntry -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.common.data.* -import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState -import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList -import me.rhunk.snapenhance.common.ui.transparentTextFieldColors -import me.rhunk.snapenhance.storage.* -import me.rhunk.snapenhance.ui.manager.Routes -import me.rhunk.snapenhance.ui.util.AlertDialogs -import me.rhunk.snapenhance.ui.util.CircularAlphaTile -import me.rhunk.snapenhance.ui.util.Dialog - -class EditThemeSection: Routes.Route() { - private var saveCallback by mutableStateOf<(() -> Unit)?>(null) - private var addEntryCallback by mutableStateOf<(key: String, initialColor: Int) -> Unit>({ _, _ -> }) - private var deleteCallback by mutableStateOf<(() -> Unit)?>(null) - private var themeColors = mutableStateListOf<ThemeColorEntry>() - - private val alertDialogs by lazy { - AlertDialogs(context.translation) - } - - override val topBarActions: @Composable (RowScope.() -> Unit) = { - var deleteConfirmationDialog by remember { mutableStateOf(false) } - - if (deleteConfirmationDialog) { - Dialog(onDismissRequest = { - deleteConfirmationDialog = false - }) { - alertDialogs.ConfirmDialog( - title = "Delete Theme", - message = "Are you sure you want to delete this theme?", - onConfirm = { - deleteCallback?.invoke() - deleteConfirmationDialog = false - }, - onDismiss = { - deleteConfirmationDialog = false - } - ) - } - } - - deleteCallback?.let { - IconButton(onClick = { - deleteConfirmationDialog = true - }) { - Icon(Icons.Default.Delete, contentDescription = null) - } - } - } - - @OptIn(ExperimentalFoundationApi::class) - override val floatingActionButton: @Composable () -> Unit = { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(5.dp), - ) { - var addAttributeDialog by remember { mutableStateOf(false) } - val attributesTranslation = remember { context.translation.getCategory("theming_attributes") } - - if (addAttributeDialog) { - AlertDialog( - title = { Text("Select an attribute to add") }, - onDismissRequest = { - addAttributeDialog = false - }, - confirmButton = {}, - text = { - var filter by remember { mutableStateOf("") } - val attributes = rememberAsyncMutableStateList(defaultValue = listOf(), keys = arrayOf(filter)) { - AvailableThemingAttributes[ThemingAttributeType.COLOR]?.filter { key -> - themeColors.none { it.key == key } && (key.contains(filter, ignoreCase = true) || attributesTranslation.getOrNull(key)?.contains(filter, ignoreCase = true) == true) - } ?: emptyList() - } - - LazyColumn( - modifier = Modifier - .fillMaxHeight(0.7f) - .fillMaxWidth(), - ) { - stickyHeader { - TextField( - modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp), - value = filter, - onValueChange = { filter = it }, - label = { Text("Search") }, - colors = transparentTextFieldColors().copy( - focusedContainerColor = MaterialTheme.colorScheme.surfaceBright, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceBright - ) - ) - } - item { - if (attributes.isEmpty()) { - Text("No attributes") - } - } - items(attributes) { attribute -> - Card( - modifier = Modifier.padding(5.dp).fillMaxWidth(), - onClick = { - addEntryCallback(attribute, Color.White.toArgb()) - addAttributeDialog = false - } - ) { - val attributeTranslation = remember(attribute) { - attributesTranslation.getOrNull(attribute) - } - - Column( - modifier = Modifier.padding(8.dp) - ) { - Text(attributeTranslation ?: attribute, lineHeight = 15.sp) - attributeTranslation?.let { - Text(attribute, fontWeight = FontWeight.Light, fontSize = 10.sp, lineHeight = 15.sp) - } - } - } - } - } - } - ) - } - - FloatingActionButton(onClick = { - addAttributeDialog = true - }) { - Icon(Icons.Default.Add, contentDescription = null) - } - - saveCallback?.let { - FloatingActionButton(onClick = { - it() - }) { - Icon(Icons.Default.Save, contentDescription = null) - } - } - } - } - - override val content: @Composable (NavBackStackEntry) -> Unit = { - val coroutineScope = rememberCoroutineScope() - val currentThemeId = remember { it.arguments?.getString("theme_id")?.toIntOrNull() } - - LaunchedEffect(Unit) { - themeColors.clear() - } - - var themeName by remember { mutableStateOf("") } - var themeDescription by remember { mutableStateOf("") } - var themeVersion by remember { mutableStateOf("1.0.0") } - var themeAuthor by remember { mutableStateOf("") } - var themeUpdateUrl by remember { mutableStateOf("") } - - val themeInfo by rememberAsyncMutableState(defaultValue = null) { - currentThemeId?.let { themeId -> - context.database.getThemeInfo(themeId)?.also { theme -> - themeName = theme.name - themeDescription = theme.description ?: "" - theme.version?.let { themeVersion = it } - themeAuthor = theme.author ?: "" - themeUpdateUrl = theme.updateUrl ?: "" - } - } - } - - val lazyListState = rememberLazyListState() - - rememberAsyncMutableState(defaultValue = DatabaseThemeContent(), keys = arrayOf(themeInfo)) { - currentThemeId?.let { - context.database.getThemeContent(it)?.also { content -> - themeColors.clear() - themeColors.addAll(content.colors) - withContext(Dispatchers.Main) { - lazyListState.scrollToItem(themeColors.size) - } - } - } ?: DatabaseThemeContent() - } - - if (themeName.isNotBlank()) { - saveCallback = { - coroutineScope.launch(Dispatchers.IO) { - val theme = DatabaseTheme( - id = currentThemeId ?: -1, - enabled = themeInfo?.enabled ?: false, - name = themeName, - description = themeDescription, - version = themeVersion, - author = themeAuthor, - updateUrl = themeUpdateUrl - ) - val themeId = context.database.addOrUpdateTheme(theme, currentThemeId) - context.database.setThemeContent(themeId, DatabaseThemeContent( - colors = themeColors - )) - withContext(Dispatchers.Main) { - routes.theming.navigateReload() - } - } - } - } else { - saveCallback = null - } - - LaunchedEffect(Unit) { - deleteCallback = null - if (currentThemeId != null) { - deleteCallback = { - coroutineScope.launch(Dispatchers.IO) { - context.database.deleteTheme(currentThemeId) - withContext(Dispatchers.Main) { - routes.theming.navigateReload() - } - } - } - } - addEntryCallback = { key, initialColor -> - coroutineScope.launch(Dispatchers.Main) { - themeColors.add(ThemeColorEntry(key, initialColor)) - delay(100) - lazyListState.scrollToItem(themeColors.size) - } - } - } - - var moreOptionsExpanded by remember { mutableStateOf(false) } - - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - val focusRequester = remember { FocusRequester() } - - TextField( - modifier = Modifier.weight(1f).focusRequester(focusRequester), - value = themeName, - onValueChange = { themeName = it }, - label = { Text("Theme Name") }, - colors = transparentTextFieldColors(), - singleLine = true, - ) - LaunchedEffect(Unit) { - if (currentThemeId == null) { - delay(200) - focusRequester.requestFocus() - } - } - IconButton( - modifier = Modifier.padding(4.dp), - onClick = { - moreOptionsExpanded = !moreOptionsExpanded - } - ) { - Icon(if (moreOptionsExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, contentDescription = null) - } - } - - if (moreOptionsExpanded) { - TextField( - modifier = Modifier.fillMaxWidth(), - maxLines = 3, - value = themeDescription, - onValueChange = { themeDescription = it }, - label = { Text("Description") }, - colors = transparentTextFieldColors() - ) - TextField( - modifier = Modifier.fillMaxWidth(), - singleLine = true, - value = themeVersion, - onValueChange = { themeVersion = it }, - label = { Text("Version") }, - colors = transparentTextFieldColors() - ) - TextField( - modifier = Modifier.fillMaxWidth(), - singleLine = true, - value = themeAuthor, - onValueChange = { themeAuthor = it }, - label = { Text("Author") }, - colors = transparentTextFieldColors() - ) - TextField( - modifier = Modifier.fillMaxWidth(), - singleLine = true, - value = themeUpdateUrl, - onValueChange = { themeUpdateUrl = it }, - label = { Text("Update URL") }, - colors = transparentTextFieldColors() - ) - } - - LazyColumn( - modifier = Modifier.fillMaxWidth(), - state = lazyListState, - contentPadding = PaddingValues(10.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - reverseLayout = true, - ) { - item { - Spacer(modifier = Modifier.height(150.dp)) - } - items(themeColors) { colorEntry -> - var showEditColorDialog by remember { mutableStateOf(false) } - var currentColor by remember { mutableIntStateOf(colorEntry.value) } - - ElevatedCard( - modifier = Modifier - .fillMaxWidth(), - onClick = { - showEditColorDialog = true - } - ) { - Row( - modifier = Modifier - .padding(4.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Default.Colorize, contentDescription = null, modifier = Modifier.padding(8.dp)) - Column( - modifier = Modifier.weight(1f) - ) { - val translation = remember(colorEntry.key) { context.translation.getOrNull("theming_attributes.${colorEntry.key}") } - Text(text = translation ?: colorEntry.key, overflow = TextOverflow.Ellipsis, maxLines = 1, lineHeight = 15.sp) - translation?.let { - Text(text = colorEntry.key, fontSize = 10.sp, fontWeight = FontWeight.Light, overflow = TextOverflow.Ellipsis, maxLines = 1, lineHeight = 15.sp) - } - } - CircularAlphaTile(selectedColor = Color(currentColor)) - } - } - - if (showEditColorDialog) { - Dialog(onDismissRequest = { showEditColorDialog = false }) { - alertDialogs.ColorPickerDialog( - initialColor = Color(currentColor), - setProperty = { - if (it == null) { - themeColors.remove(colorEntry) - return@ColorPickerDialog - } - currentColor = it.toArgb() - colorEntry.value = currentColor - }, - dismiss = { - showEditColorDialog = false - } - ) - } - } - } - item { - if (themeColors.isEmpty()) { - Text("No colors added yet", modifier = Modifier - .fillMaxWidth() - .padding(8.dp), fontWeight = FontWeight.Light, textAlign = TextAlign.Center) - } - } - } - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/ThemeCatalog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/ThemeCatalog.kt @@ -1,277 +0,0 @@ -package me.rhunk.snapenhance.ui.manager.pages.theming - -import android.net.Uri -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Palette -import androidx.compose.material3.* -import androidx.compose.runtime.* -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.TextDecoration -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.net.toUri -import kotlinx.coroutines.* -import me.rhunk.snapenhance.common.data.RepositoryIndex -import me.rhunk.snapenhance.common.ui.AsyncUpdateDispatcher -import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState -import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList -import me.rhunk.snapenhance.storage.getThemeList -import me.rhunk.snapenhance.storage.getRepositories -import me.rhunk.snapenhance.storage.getThemeIdByUpdateUrl -import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator -import me.rhunk.snapenhance.ui.util.pullrefresh.pullRefresh -import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState -import okhttp3.Request - - -private val cachedRepoIndexes = mutableStateMapOf<String, RepositoryIndex>() -private val cacheReloadDispatcher = AsyncUpdateDispatcher() - -@Composable -fun ThemeCatalog(root: ThemingRoot) { - val context = remember { root.context } - val coroutineScope = rememberCoroutineScope { Dispatchers.IO } - - fun fetchRepoIndexes(): Map<String, RepositoryIndex> { - val indexes = mutableMapOf<String, RepositoryIndex>() - - context.database.getRepositories().forEach { rootUri -> - val indexUri = rootUri.toUri().buildUpon().appendPath("index.json").build() - - runCatching { - root.okHttpClient.newCall( - Request.Builder().url(indexUri.toString()).build() - ).execute().use { response -> - if (!response.isSuccessful) { - context.log.error("Failed to fetch theme index from $indexUri: ${response.code}") - context.shortToast("Failed to fetch index of $indexUri") - return@forEach - } - - runCatching { - indexes[rootUri] = context.gson.fromJson(response.body.charStream(), RepositoryIndex::class.java) - }.onFailure { - context.log.error("Failed to parse theme index from $indexUri", it) - context.shortToast("Failed to parse index of $indexUri") - } - } - }.onFailure { - context.log.error("Failed to fetch theme index from $indexUri", it) - context.shortToast("Failed to fetch index of $indexUri") - } - } - - return indexes - } - - suspend fun installTheme(themeUri: Uri) { - root.okHttpClient.newCall( - Request.Builder().url(themeUri.toString()).build() - ).execute().use { response -> - if (!response.isSuccessful) { - context.log.error("Failed to fetch theme from $themeUri: ${response.code}") - context.shortToast("Failed to fetch theme from $themeUri") - return - } - - val themeContent = response.body.bytes().toString(Charsets.UTF_8) - root.importTheme(themeContent, themeUri.toString()) - } - } - - var isRefreshing by remember { mutableStateOf(false) } - - suspend fun refreshCachedIndexes() { - isRefreshing = true - coroutineScope { - launch(Dispatchers.IO) { - fetchRepoIndexes().let { - context.log.verbose("Fetched ${it.size} theme indexes") - it.forEach { (t, u) -> - context.log.verbose("Fetched theme index from $t with ${u.themes.size} themes") - } - synchronized(cachedRepoIndexes) { - cachedRepoIndexes.clear() - cachedRepoIndexes += it - } - cacheReloadDispatcher.dispatch() - delay(600) - isRefreshing = false - } - } - } - } - - val installedThemes = rememberAsyncMutableStateList(defaultValue = listOf(), updateDispatcher = root.localReloadDispatcher, keys = arrayOf(cachedRepoIndexes)) { - context.database.getThemeList() - } - - val remoteThemes by rememberAsyncMutableState(defaultValue = listOf(), updateDispatcher = cacheReloadDispatcher, keys = arrayOf(root.searchFilter.value)) { - cachedRepoIndexes.entries.flatMap { - it.value.themes.map { theme -> it.key to theme } - }.let { - val filter = root.searchFilter.value - if (filter.isNotBlank()) { - it.filter { (_, theme) -> - theme.name.contains(filter, ignoreCase = true) || theme.description?.contains(filter, ignoreCase = true) == true - } - } else it - } - } - - LaunchedEffect(Unit) { - if (cachedRepoIndexes.isNotEmpty()) return@LaunchedEffect - isRefreshing = true - coroutineScope.launch { - refreshCachedIndexes() - } - } - - val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh = { - coroutineScope.launch { - refreshCachedIndexes() - } - }) - - Box( - modifier = Modifier.fillMaxSize() - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .pullRefresh(pullRefreshState), - contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - item { - if (remoteThemes.isEmpty()) { - Text( - text = "No themes available", - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - textAlign = TextAlign.Center, - fontSize = 15.sp, - fontWeight = FontWeight.Light - ) - } - } - items(remoteThemes, key = { it.first + it.second.hashCode() }) { (_, themeManifest) -> - val themeUri = remember { - cachedRepoIndexes.entries.find { it.value.themes.contains(themeManifest) }?.key?.toUri()?.buildUpon()?.appendPath(themeManifest.filepath)?.build() - } - - val hasUpdate by rememberAsyncMutableState(defaultValue = false, keys = arrayOf(themeManifest)) { - installedThemes.takeIf { themeUri != null }?.find { it.updateUrl == themeUri.toString() }?.let { installedTheme -> - installedTheme.version != themeManifest.version - } ?: false - } - - var isInstalling by rememberAsyncMutableState(defaultValue = false, keys = arrayOf(themeManifest)) { - false - } - - var isInstalled by rememberAsyncMutableState(defaultValue = true, keys = arrayOf(themeManifest)) { - context.database.getThemeIdByUpdateUrl(themeUri.toString()) != null - } - - ElevatedCard(onClick = { - //TODO: Show theme details - }) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Default.Palette, contentDescription = null, modifier = Modifier.padding(16.dp)) - Column( - modifier = Modifier.weight(1f), - ) { - Text( - text = themeManifest.name, - maxLines = 1, - fontSize = 16.sp, - lineHeight = 10.sp, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.Bold, - ) - themeManifest.author?.let { - Text( - text = "by $it", - maxLines = 1, - fontSize = 10.sp, - lineHeight = 16.sp, - textDecoration = TextDecoration.Underline, - fontWeight = FontWeight.Light, - overflow = TextOverflow.Visible, - ) - } - themeManifest.description?.let { - Text( - text = it, - fontSize = 12.sp, - maxLines = 3, - lineHeight = 16.sp, - overflow = TextOverflow.Ellipsis, - ) - } - if (hasUpdate) { - Text( - text = "Version ${themeManifest.version} available", - fontWeight = FontWeight.Bold - ) - } - } - Row( - verticalAlignment = Alignment.CenterVertically - ) { - if (isInstalling) { - CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp) - } else { - Button( - enabled = !isInstalled || hasUpdate, - onClick = { - isInstalling = true - context.coroutineScope.launch { - runCatching { - installTheme(themeUri ?: throw IllegalStateException("Failed to get theme URI")) - isInstalled = true - }.onFailure { - context.log.error("Failed to install theme ${themeManifest.name}", it) - context.shortToast("Failed to install theme ${themeManifest.name}. ${it.message}") - } - isInstalling = false - } - } - ) { - if (hasUpdate) { - Text("Update") - } else { - Text(if (isInstalled) "Installed" else "Install") - } - } - } - } - } - } - } - item { - Spacer(modifier = Modifier.height(80.dp)) - } - } - - PullRefreshIndicator( - refreshing = isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter) - ) - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/ThemingRoot.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/theming/ThemingRoot.kt @@ -1,462 +0,0 @@ -package me.rhunk.snapenhance.ui.manager.pages.theming - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -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.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.core.net.toUri -import androidx.navigation.NavBackStackEntry -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.common.data.DatabaseTheme -import me.rhunk.snapenhance.common.data.DatabaseThemeContent -import me.rhunk.snapenhance.common.data.ExportedTheme -import me.rhunk.snapenhance.common.ui.AsyncUpdateDispatcher -import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList -import me.rhunk.snapenhance.common.ui.transparentTextFieldColors -import me.rhunk.snapenhance.common.util.ktx.getUrlFromClipboard -import me.rhunk.snapenhance.storage.* -import me.rhunk.snapenhance.ui.manager.Routes -import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper -import me.rhunk.snapenhance.ui.util.openFile -import me.rhunk.snapenhance.ui.util.pagerTabIndicatorOffset -import me.rhunk.snapenhance.ui.util.saveFile -import okhttp3.OkHttpClient - -class ThemingRoot: Routes.Route() { - val localReloadDispatcher = AsyncUpdateDispatcher() - private lateinit var activityLauncherHelper: ActivityLauncherHelper - - private var currentPage by mutableIntStateOf(0) - val okHttpClient by lazy { OkHttpClient() } - val searchFilter = mutableStateOf("") - - - private fun exportTheme(theme: DatabaseTheme) { - context.coroutineScope.launch { - val exportedTheme = theme.toExportedTheme(context.database.getThemeContent(theme.id) ?: DatabaseThemeContent()) - - activityLauncherHelper.saveFile(theme.name.replace(" ", "_").lowercase() + ".json") { uri -> - runCatching { - context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { outputStream -> - outputStream.write(context.gson.toJson(exportedTheme).toByteArray()) - outputStream.flush() - } - context.shortToast("Theme exported successfully") - }.onFailure { - context.log.error("Failed to save theme", it) - context.longToast("Failed to export theme! Check logs for more details") - } - } - } - } - - private fun duplicateTheme(theme: DatabaseTheme) { - context.coroutineScope.launch { - val themeId = context.database.addOrUpdateTheme(theme.copy( - updateUrl = null - )) - context.database.setThemeContent(themeId, context.database.getThemeContent(theme.id) ?: DatabaseThemeContent()) - context.shortToast("Theme duplicated successfully") - withContext(Dispatchers.Main) { - localReloadDispatcher.dispatch() - } - } - } - - suspend fun importTheme(content: String, updateUrl: String? = null) { - val theme = context.gson.fromJson(content, ExportedTheme::class.java) - val existingTheme = updateUrl?.let { - context.database.getThemeIdByUpdateUrl(it) - }?.let { - context.database.getThemeInfo(it) - } - val databaseTheme = theme.toDatabaseTheme( - updateUrl = updateUrl, - enabled = existingTheme?.enabled ?: false - ) - - val themeId = context.database.addOrUpdateTheme( - themeId = existingTheme?.id, - theme = databaseTheme - ) - - context.database.setThemeContent(themeId, theme.content) - context.shortToast("Theme imported successfully") - withContext(Dispatchers.Main) { - localReloadDispatcher.dispatch() - } - } - - private fun importTheme() { - activityLauncherHelper.openFile { uri -> - context.coroutineScope.launch { - runCatching { - val themeJson = context.androidContext.contentResolver.openInputStream(uri.toUri())?.bufferedReader().use { - it?.readText() - } ?: throw Exception("Failed to read file") - - importTheme(themeJson) - }.onFailure { - context.log.error("Failed to import theme", it) - context.longToast("Failed to import theme! Check logs for more details") - } - } - } - } - - private suspend fun importFromURL(url: String) { - val result = okHttpClient.newCall( - okhttp3.Request.Builder() - .url(url) - .build() - ).execute() - - if (!result.isSuccessful) { - throw Exception("Failed to fetch theme from URL ${result.message}") - } - - importTheme(result.body.string(), url) - } - - override val init: () -> Unit = { - activityLauncherHelper = ActivityLauncherHelper(context.activity!!) - } - - override val topBarActions: @Composable (RowScope.() -> Unit) = { - var showSearchBar by remember { mutableStateOf(false) } - val focusRequester = remember { FocusRequester() } - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - if (showSearchBar) { - OutlinedTextField( - value = searchFilter.value, - onValueChange = { searchFilter.value = it }, - placeholder = { Text("Search") }, - modifier = Modifier - .weight(1f) - .focusRequester(focusRequester) - .onGloballyPositioned { - focusRequester.requestFocus() - }, - colors = transparentTextFieldColors() - ) - DisposableEffect(Unit) { - onDispose { - searchFilter.value = "" - } - } - } - IconButton(onClick = { - showSearchBar = !showSearchBar - }) { - Icon(if (showSearchBar) Icons.Default.Close else Icons.Default.Search, contentDescription = null) - } - } - } - - override val floatingActionButton: @Composable () -> Unit = { - var showImportFromUrlDialog by remember { mutableStateOf(false) } - - if (showImportFromUrlDialog) { - var url by remember { mutableStateOf("") } - var loading by remember { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = { showImportFromUrlDialog = false }, - title = { Text("Import theme from URL") }, - text = { - val focusRequester = remember { FocusRequester() } - TextField( - value = url, - onValueChange = { url = it }, - label = { Text("URL") }, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .onGloballyPositioned { - focusRequester.requestFocus() - } - ) - LaunchedEffect(Unit) { - context.androidContext.getUrlFromClipboard()?.let { - url = it - } - } - }, - confirmButton = { - Button( - enabled = url.isNotBlank() && !loading, - onClick = { - loading = true - context.coroutineScope.launch { - runCatching { - importFromURL(url) - withContext(Dispatchers.Main) { - showImportFromUrlDialog = false - } - }.onFailure { - context.log.error("Failed to import theme", it) - context.longToast("Failed to import theme! ${it.message}") - } - withContext(Dispatchers.Main) { - loading = false - } - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Import") - } - } - ) - } - Column( - horizontalAlignment = Alignment.End - ) { - when (currentPage) { - 0 -> { - ExtendedFloatingActionButton( - onClick = { - routes.editTheme.navigate() - }, - icon = { - Icon(Icons.Default.Add, contentDescription = null) - }, - text = { - Text("New theme") - } - ) - Spacer(modifier = Modifier.height(8.dp)) - ExtendedFloatingActionButton( - onClick = { - importTheme() - }, - icon = { - Icon(Icons.Default.Upload, contentDescription = null) - }, - text = { - Text("Import from file") - } - ) - Spacer(modifier = Modifier.height(8.dp)) - ExtendedFloatingActionButton( - onClick = { showImportFromUrlDialog = true }, - icon = { - Icon(Icons.Default.Link, contentDescription = null) - }, - text = { - Text("Import from URL") - } - ) - } - 1 -> { - ExtendedFloatingActionButton( - onClick = { - routes.manageRepos.navigate() - }, - icon = { - Icon(Icons.Default.Public, contentDescription = null) - }, - text = { - Text("Manage repositories") - } - ) - } - } - } - } - - @Composable - private fun InstalledThemes() { - val themes = rememberAsyncMutableStateList(defaultValue = listOf(), updateDispatcher = localReloadDispatcher, keys = arrayOf(searchFilter.value)) { - context.database.getThemeList().let { - val filter = searchFilter.value - if (filter.isNotBlank()) { - it.filter { theme -> - theme.name.contains(filter, ignoreCase = true) || - theme.author?.contains(filter, ignoreCase = true) == true || - theme.description?.contains(filter, ignoreCase = true) == true - } - } else it - } - } - - LazyColumn( - modifier = Modifier - .fillMaxSize(), - contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - item { - if (themes.isEmpty()) { - Text( - text = translation["no_themes_hint"], - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - textAlign = TextAlign.Center, - fontSize = 15.sp, - fontWeight = FontWeight.Light - ) - } - } - items(themes, key = { it.id }) { theme -> - var showSettings by remember(theme) { mutableStateOf(false) } - - ElevatedCard( - modifier = Modifier - .fillMaxWidth(), - onClick = { - routes.editTheme.navigate { - this["theme_id"] = theme.id.toString() - } - } - ) { - Row( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - Icons.Default.Palette, contentDescription = null, modifier = Modifier.padding(5.dp)) - Column( - modifier = Modifier - .weight(1f) - .padding(8.dp), - ) { - Text(text = theme.name, fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 20.sp) - theme.author?.takeIf { it.isNotBlank() }?.let { - Text(text = "by $it", lineHeight = 15.sp, fontWeight = FontWeight.Light, fontSize = 12.sp) - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - var state by remember { mutableStateOf(theme.enabled) } - - IconButton(onClick = { - showSettings = true - }) { - Icon(Icons.Default.Settings, contentDescription = null) - } - - Switch(checked = state, onCheckedChange = { - state = it - context.database.setThemeState(theme.id, it) - }) - } - } - } - - if (showSettings) { - val actionsRow = remember { - mapOf( - ("Duplicate" to Icons.Default.ContentCopy) to { duplicateTheme(theme) }, - ("Export" to Icons.Default.Download) to { exportTheme(theme) } - ) - } - AlertDialog( - onDismissRequest = { showSettings = false }, - title = { Text("Theme settings") }, - text = { - Column( - modifier = Modifier.fillMaxWidth(), - ) { - actionsRow.forEach { entry -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - showSettings = false - entry.value() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Icon(entry.key.second, contentDescription = null, modifier = Modifier.padding(16.dp)) - Spacer(modifier = Modifier.width(5.dp)) - Text(entry.key.first) - } - } - } - }, - confirmButton = {} - ) - } - } - item { - Spacer(modifier = Modifier.height(200.dp)) - } - } - } - - - @OptIn(ExperimentalFoundationApi::class) - override val content: @Composable (NavBackStackEntry) -> Unit = { - val coroutineScope = rememberCoroutineScope() - val titles = remember { listOf("Installed Themes", "Catalog") } - val pagerState = rememberPagerState { titles.size } - currentPage = pagerState.currentPage - - Column { - TabRow(selectedTabIndex = pagerState.currentPage, indicator = { tabPositions -> - TabRowDefaults.SecondaryIndicator( - Modifier.pagerTabIndicatorOffset( - pagerState = pagerState, - tabPositions = tabPositions - ) - ) - }) { - titles.forEachIndexed { index, title -> - Tab( - selected = pagerState.currentPage == index, - onClick = { - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { - Text( - text = title, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - ) - } - } - - HorizontalPager( - modifier = Modifier.weight(1f), - state = pagerState - ) { page -> - when (page) { - 0 -> InstalledThemes() - 1 -> ThemeCatalog(this@ThemingRoot) - } - } - } - } -}- \ No newline at end of file diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -37,8 +37,6 @@ "friend_tracker": "Friend Tracker", "edit_rule": "Edit Rule", "file_imports": "File Imports", - "theming": "Theming", - "edit_theme": "Edit Theme", "manage_repos": "Manage Repositories", "social": "Social", "manage_scope": "Manage Scope", @@ -189,9 +187,6 @@ "search_bar": "Search", "no_friends_map": "No friends on the map", "no_friends_found": "No friends found" - }, - "theming": { - "no_themes_hint": "No themes found" } }, "dialogs": { @@ -336,10 +331,6 @@ "logger_history": { "name": "Logger History", "description": "View the history of logged messages" - }, - "theming": { - "name": "Theming", - "description": "Customize the look and feel of Snapchat" } }, @@ -454,10 +445,6 @@ "name": "Enable App Appearance Settings", "description": "Enables the hidden App Appearance Setting\nMay not be required on newer Snapchat versions" }, - "custom_theme": { - "name": "Custom Theme", - "description": "Customize Snapchat's Colors\nNote: if you choose a dark theme (like Amoled), you may need to enable the dark mode in Snapchat settings for better results" - }, "friend_feed_message_preview": { "name": "Friend Feed Message Preview", "description": "Shows a preview of the last messages in the Friend Feed", @@ -1212,12 +1199,6 @@ "always_light": "Always Light", "always_dark": "Always Dark" }, - "custom_theme": { - "amoled_dark_mode": "Amoled Dark Mode", - "custom": "Custom Themes (Use the Quick Actions to manage themes)", - "material_you_light": "Material You Light (Android 12+)", - "material_you_dark": "Material You Dark (Android 12+)" - }, "friend_feed_menu_buttons": { "auto_download": "\u2B07\uFE0F Auto Download", "auto_save": "\uD83D\uDCAC Auto Save Messages", @@ -1708,48 +1689,6 @@ "date_range_input_invalid_range_input": "Invalid date range" }, - "theming_attributes": { - "sigColorTextPrimary": "Main Text Color", - "sigColorChatChat": "Main Friend Feed Text Color", - "sigColorBackgroundSurface": "Background Surface Color", - "sigColorChatPendingSending": "Secondary Friend Feed Text Color", - "sigColorChatSnapWithSound": "Snaps With Sound Text Color", - "sigColorChatSnapWithoutSound": "Snaps Without Sound Text Color", - "actionSheetDescriptionTextColor": "Action Menu Description Text Color", - "sigColorBackgroundMain": "Background Color", - "listBackgroundDrawable": "Conversation list Background", - "sigColorChatConversationsLine": "Conversations Line Color", - "actionSheetBackgroundDrawable": "Action Menu Background Color", - "actionSheetRoundedBackgroundDrawable": "Action Menu Round Background Color", - "sigColorIconPrimary": "Action Menu Icon Color", - "sigExceptionColorCameraGridLines": "Camera Gridlines Color", - "listDivider": "List Divider Color", - "sigColorIconSecondary": "Secondary Icon Color", - "itemShapeFillColor": "Item Shape Fill Color", - "ringColor": "Ring Color", - "ringStartColor": "Ring Start Color", - "sigColorLayoutPlaceholder": "Layout Placeholder Color", - "scButtonColor": "Snapchat Button Color", - "recipientPillBackgroundDrawable": "Recipient Pill Background", - "boxBackgroundColor": "Box Background Color", - "editTextColor": "Edit Text Color", - "chipBackgroundColor": "Chip Background Color", - "recipientInputStyle": "Recipient Input Style", - "rangeFillColor": "Range Fill Color", - "pstsIndicatorColor": "PSTS Indicator Color", - "pstsTabBackground": "PSTS Tab Background", - "pstsDividerColor": "PSTS Divider Color", - "tabTextColor": "Tab Text Color", - "statusBarForeground": "Status Bar Foreground Color", - "statusBarBackground": "Status Bar Background Color", - "strokeColor": "Stroke Color", - "storyReplayViewRingColor": "Story Replay View Ring Color", - "sigColorButtonPrimary": "Primary Button Color", - "sigColorBaseAppYellow": "Base App Yellow Color", - "sigColorBackgroundSurfaceTranslucent": "Translucent Background Surface Color", - "sigColorStoryRingFriendsFeedStoryRing": "Story Ring Friends Feed Story Ring Color", - "sigColorStoryRingDiscoverTabThumbnailStoryRing": "Story Ring Discover Tab Thumbnail Story Ring Color" - }, "send_override_dialog": { "title": "Send media as {type}", "duration": "Duration: {duration}", diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/bridge/BridgeFiles.kt @@ -5,6 +5,8 @@ import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor.AutoCloseInputStream import android.os.ParcelFileDescriptor.AutoCloseOutputStream import me.rhunk.snapenhance.bridge.storage.FileHandle +import me.rhunk.snapenhance.common.bridge.FileHandleScope.entries +import me.rhunk.snapenhance.common.bridge.InternalFileHandleType.entries import me.rhunk.snapenhance.common.util.LazyBridgeValue import me.rhunk.snapenhance.common.util.lazyBridge import java.io.File @@ -16,8 +18,7 @@ enum class FileHandleScope( INTERNAL("internal"), LOCALE("locale"), USER_IMPORT("user_import"), - COMPOSER("composer"), - THEME("theme"); + COMPOSER("composer"); companion object { fun fromValue(name: String): FileHandleScope? = entries.find { it.key == name } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/UserInterfaceTweaks.kt @@ -27,12 +27,6 @@ class UserInterfaceTweaks : ConfigContainer() { set(mutableListOf("conversation_info", MessagingRuleType.STEALTH.key)) } val autoCloseFriendFeedMenu = boolean("auto_close_friend_feed_menu") - val customTheme = unique("custom_theme", - "amoled_dark_mode", - "material_you_light", - "material_you_dark", - "custom", - ) { addNotices(FeatureNotice.UNSTABLE); requireRestart(); versionCheck = RES_OBF_VERSION_CHECK.copy(isDisabled = true) } val friendFeedMessagePreview = container("friend_feed_message_preview", FriendFeedMessagePreview()) { requireRestart() } val snapPreview = boolean("snap_preview") { addNotices(FeatureNotice.UNSTABLE); requireRestart() } val bootstrapOverride = container("bootstrap_override", BootstrapOverride()) { requireRestart() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt @@ -127,7 +127,6 @@ class FeatureManager( AccountSwitcher(), RemoveGroupsLockedStatus(), BypassMessageActionRestrictions(), - CustomTheming(), BetterLocation(), MediaFilePicker(), HideActiveMusic(), diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomTheming.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/CustomTheming.kt @@ -1,130 +0,0 @@ -package me.rhunk.snapenhance.core.features.impl.ui - -import android.content.res.TypedArray -import android.os.Build -import android.os.ParcelFileDescriptor -import android.os.ParcelFileDescriptor.AutoCloseInputStream -import android.util.TypedValue -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.ui.graphics.toArgb -import com.google.gson.reflect.TypeToken -import me.rhunk.snapenhance.common.bridge.FileHandleScope -import me.rhunk.snapenhance.common.data.DatabaseThemeContent -import me.rhunk.snapenhance.core.features.Feature -import me.rhunk.snapenhance.core.util.hook.HookStage -import me.rhunk.snapenhance.core.util.hook.hook -import me.rhunk.snapenhance.core.util.ktx.getIdentifier -import me.rhunk.snapenhance.core.util.ktx.getObjectField - -class CustomTheming: Feature("Custom Theming") { - private fun getAttribute(name: String): Int { - return context.resources.getIdentifier(name, "attr") - } - - private fun parseAttributeList(vararg attributes: Pair<String, Number>): Map<Int, Int> { - return attributes.toMap().mapKeys { - getAttribute(it.key) - }.filterKeys { it != 0 }.mapValues { - it.value.toInt() - } - } - - override fun init() { - val customThemeName = context.config.userInterface.customTheme.getNullable() ?: return - var currentTheme = mapOf<Int, Int>() // resource id -> color - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val colorScheme = dynamicDarkColorScheme(context.androidContext) - val light = customThemeName == "material_you_light" - val surfaceVariant = (if (light) colorScheme.surfaceVariant else colorScheme.onSurfaceVariant).toArgb() - val background = (if (light) colorScheme.onBackground else colorScheme.background).toArgb() - - currentTheme = parseAttributeList( - "sigColorTextPrimary" to surfaceVariant, - "sigColorChatChat" to surfaceVariant, - "sigColorChatPendingSending" to surfaceVariant, - "sigColorChatSnapWithSound" to surfaceVariant, - "sigColorChatSnapWithoutSound" to surfaceVariant, - "sigColorBackgroundMain" to background, - "sigColorBackgroundSurface" to background, - "listDivider" to colorScheme.onPrimary.copy(alpha = 0.12f).toArgb(), - "actionSheetBackgroundDrawable" to background, - "actionSheetRoundedBackgroundDrawable" to background, - "sigExceptionColorCameraGridLines" to background, - ) - } - - if (customThemeName == "amoled_dark_mode") { - currentTheme = parseAttributeList( - "sigColorTextPrimary" to 0xFFFFFFFF, - "sigColorChatChat" to 0xFFFFFFFF, - "sigColorChatPendingSending" to 0xFFFFFFFF, - "sigColorChatSnapWithSound" to 0xFFFFFFFF, - "sigColorChatSnapWithoutSound" to 0xFFFFFFFF, - "sigColorBackgroundMain" to 0xFF000000, - "sigColorBackgroundSurface" to 0xFF000000, - "listDivider" to 0xFF000000, - "actionSheetBackgroundDrawable" to 0xFF000000, - "actionSheetRoundedBackgroundDrawable" to 0xFF000000, - "sigExceptionColorCameraGridLines" to 0xFF000000, - ) - } - - if (customThemeName == "custom") { - val availableThemes = context.fileHandlerManager.getFileHandle(FileHandleScope.THEME.key, "")?.open(ParcelFileDescriptor.MODE_READ_ONLY)?.use { pfd -> - AutoCloseInputStream(pfd).use { it.readBytes() } - }?.let { - context.gson.fromJson(it.toString(Charsets.UTF_8), object: TypeToken<List<DatabaseThemeContent>>() {}) - } ?: run { - context.log.verbose("no custom themes found") - return - } - - val customThemeColors = mutableMapOf<Int, Int>() - - context.log.verbose("loading ${availableThemes.size} custom themes") - - availableThemes.forEach { themeContent -> - themeContent.colors.forEach colors@{ colorEntry -> - customThemeColors[getAttribute(colorEntry.key).takeIf { it != 0 }.also { - if (it == null) { - context.log.warn("unknown color attribute: ${colorEntry.key}") - } - } ?: return@colors] = colorEntry.value - } - } - - currentTheme = customThemeColors - - context.log.verbose("loaded ${customThemeColors.size} custom theme colors") - } - - onNextActivityCreate { - if (currentTheme.isEmpty()) return@onNextActivityCreate - - context.androidContext.theme.javaClass.getMethod("obtainStyledAttributes", IntArray::class.java).hook( - HookStage.AFTER) { param -> - val array = param.arg<IntArray>(0) - val customColor = (currentTheme[array[0]] as? Number)?.toInt() ?: return@hook - - val result = param.getResult() as TypedArray - val typedArrayData = result.getObjectField("mData") as IntArray - - when (val attributeType = result.getType(0)) { - TypedValue.TYPE_INT_COLOR_ARGB8, TypedValue.TYPE_INT_COLOR_RGB8, TypedValue.TYPE_INT_COLOR_ARGB4, TypedValue.TYPE_INT_COLOR_RGB4 -> { - typedArrayData[1] = customColor // index + STYLE_DATA - } - TypedValue.TYPE_STRING -> { - val stringValue = result.getString(0) - if (stringValue?.endsWith(".xml") == true) { - typedArrayData[0] = TypedValue.TYPE_INT_COLOR_ARGB4 // STYLE_TYPE - typedArrayData[1] = customColor // STYLE_DATA - typedArrayData[5] = 0; // STYLE_DENSITY - } - } - else -> context.log.warn("unknown attribute type: ${attributeType.toString(16)}") - } - } - } - } -}- \ No newline at end of file