commit 16a16df51a573db6d439e95181c51c540bb99fe5
parent 31570694a015c91a8fa67cdbfd2e1b58531033cf
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Mon, 21 Aug 2023 18:09:59 +0200

feat: partial scope content

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 14++++++++++++--
Mapp/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt | 23+++++++++++++++++++----
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/MainActivity.kt | 3++-
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Navigation.kt | 9+++++++++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt | 3+++
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/features/FeaturesSection.kt | 53+++++++++++++++++++++++++++++------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mapp/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt | 3+--
Mcore/src/main/aidl/me/rhunk/snapenhance/bridge/SyncCallback.aidl | 4++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 2++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt | 2+-
11 files changed, 234 insertions(+), 57 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -33,13 +33,23 @@ class BridgeService : Service() { } fun triggerFriendSync(friendId: String) { - SerializableDataObject.fromJson<FriendInfo>(syncCallback.syncFriend(friendId)).let { + val syncedFriend = syncCallback.syncFriend(friendId) + if (syncedFriend == null) { + Logger.error("Failed to sync friend $friendId") + return + } + SerializableDataObject.fromJson<FriendInfo>(syncedFriend).let { remoteSideContext.modDatabase.syncFriend(it) } } fun triggerGroupSync(groupId: String) { - SerializableDataObject.fromJson<MessagingGroupInfo>(syncCallback.syncGroup(groupId)).let { + val syncedGroup = syncCallback.syncGroup(groupId) + if (syncedGroup == null) { + Logger.error("Failed to sync group $groupId") + return + } + SerializableDataObject.fromJson<MessagingGroupInfo>(syncedGroup).let { remoteSideContext.modDatabase.syncGroupInfo(it) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/messaging/ModDatabase.kt @@ -25,7 +25,13 @@ class ModDatabase( var receiveMessagingDataCallback: (friends: List<MessagingFriendInfo>, groups: List<MessagingGroupInfo>) -> Unit = { _, _ -> } fun executeAsync(block: () -> Unit) { - executor.execute(block) + executor.execute { + runCatching { + block() + }.onFailure { + Logger.error("Failed to execute async block", it) + } + } } fun init() { @@ -56,7 +62,7 @@ class ModDatabase( "userId VARCHAR PRIMARY KEY", "notify BOOLEAN", "expirationTimestamp BIGINT", - "count INTEGER" + "length INTEGER" ), "analytics_config" to listOf( "userId VARCHAR PRIMARY KEY", @@ -137,7 +143,7 @@ class ModDatabase( ) //sync streaks if (friend.streakLength > 0) { - database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, count) VALUES (?, ?, ?)", arrayOf( + database.execSQL("INSERT OR REPLACE INTO streaks (userId, expirationTimestamp, length) VALUES (?, ?, ?)", arrayOf( friend.userId, friend.streakExpirationTimestamp, friend.streakLength @@ -227,8 +233,17 @@ class ModDatabase( userId = cursor.getStringOrNull("userId")!!, notify = cursor.getInteger("notify") == 1, expirationTimestamp = cursor.getLongOrNull("expirationTimestamp") ?: 0L, - count = cursor.getInteger("count") + length = cursor.getInteger("length") ) } } + + fun setFriendStreaksNotify(userId: String, notify: Boolean) { + executeAsync { + database.execSQL("UPDATE streaks SET notify = ? WHERE userId = ?", arrayOf( + if (notify) 1 else 0, + userId + )) + } + } } \ 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 @@ -44,7 +44,8 @@ class MainActivity : ComponentActivity() { Scaffold( containerColor = MaterialTheme.colorScheme.background, topBar = { navigation.TopBar() }, - bottomBar = { navigation.NavBar() } + bottomBar = { navigation.NavBar() }, + floatingActionButton = { navigation.Fab() } ) { innerPadding -> navigation.NavigationHost( innerPadding = innerPadding, 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 @@ -101,6 +101,15 @@ class Navigation( } @Composable + fun Fab() { + val navBackStackEntry by navHostController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination ?: return + val currentSection = getCurrentSection(currentDestination) + + currentSection.FloatingActionButton() + } + + @Composable fun NavBar() { NavigationBar { val navBackStackEntry by navHostController.currentBackStackEntryAsState() 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 @@ -76,6 +76,9 @@ open class Section { @Composable open fun TopBarActions(rowScope: RowScope) {} + @Composable + open fun FloatingActionButton() {} + open fun build(navGraphBuilder: NavGraphBuilder) { navGraphBuilder.composable(enumSection.route) { Content() 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 @@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.rounded.Save +import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledIconButton @@ -77,6 +78,7 @@ import me.rhunk.snapenhance.core.config.PropertyValue import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.ChooseFolderHelper +@OptIn(ExperimentalMaterial3Api::class) class FeaturesSection : Section() { private val dialogs by lazy { Dialogs(context.translation) } @@ -91,6 +93,8 @@ class FeaturesSection : Section() { private val featuresRouteName by lazy { context.translation["manager.routes.features"] } + private lateinit var rememberScaffoldState: BottomSheetScaffoldState + private val allContainers by lazy { val containers = mutableMapOf<String, PropertyPair<*>>() fun queryContainerRecursive(container: ConfigContainer) { @@ -458,35 +462,14 @@ class FeaturesSection : Section() { } } - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PropertiesView( properties: List<PropertyPair<*>> ) { - val scope = rememberCoroutineScope() - val scaffoldState = rememberBottomSheetScaffoldState() + rememberScaffoldState = rememberBottomSheetScaffoldState() Scaffold( - snackbarHost = { SnackbarHost(scaffoldState.snackbarHostState) }, + snackbarHost = { SnackbarHost(rememberScaffoldState.snackbarHostState) }, modifier = Modifier.fillMaxSize(), - floatingActionButton = { - FloatingActionButton( - onClick = { - context.config.writeConfig() - scope.launch { - scaffoldState.snackbarHostState.showSnackbar("Saved") - } - }, - modifier = Modifier.padding(10.dp), - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - shape = RoundedCornerShape(16.dp), - ) { - Icon( - imageVector = Icons.Rounded.Save, - contentDescription = null - ) - } - }, content = { innerPadding -> LazyColumn( modifier = Modifier @@ -494,7 +477,7 @@ class FeaturesSection : Section() { .padding(innerPadding), //save button space contentPadding = PaddingValues(top = 10.dp, bottom = 110.dp), - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Top ) { items(properties) { PropertyCard(it) @@ -504,6 +487,28 @@ class FeaturesSection : Section() { ) } + @Composable + override fun FloatingActionButton() { + val scope = rememberCoroutineScope() + FloatingActionButton( + onClick = { + context.config.writeConfig() + scope.launch { + rememberScaffoldState.snackbarHostState.showSnackbar("Saved") + } + }, + modifier = Modifier.padding(10.dp), + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Rounded.Save, + contentDescription = null + ) + } + } + @Composable private fun Container( diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/ScopeContent.kt @@ -3,7 +3,13 @@ package me.rhunk.snapenhance.ui.manager.sections.social import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -13,13 +19,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation.NavController import kotlinx.coroutines.launch import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.core.messaging.MessagingRuleType import me.rhunk.snapenhance.core.messaging.SocialScope +import me.rhunk.snapenhance.ui.util.BitmojiImage +import me.rhunk.snapenhance.util.snap.BitmojiSelfie class ScopeContent( private val context: RemoteSideContext, @@ -28,7 +39,6 @@ class ScopeContent( private val scope: SocialScope, private val id: String ) { - @Composable private fun DeleteScopeEntityButton() { val coroutineScope = rememberCoroutineScope() @@ -40,7 +50,7 @@ class ScopeContent( context.modDatabase.executeAsync { coroutineScope.launch { section.onResumed() - navController.popBackStack() + navController.navigate(SocialSection.MAIN_ROUTE) } } }) { @@ -50,7 +60,10 @@ class ScopeContent( @Composable fun Content() { - Column { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + ) { when (scope) { SocialScope.FRIEND -> Friend() SocialScope.GROUP -> Group() @@ -60,42 +73,162 @@ class ScopeContent( val scopeRules = context.modDatabase.getRulesFromId(scope, id) - Text(text = "Rules", maxLines = 1) - Spacer(modifier = Modifier.height(16.dp)) + SectionTitle("Rules") - //manager anti features etc - MessagingRuleType.values().forEach { feature -> - var featureEnabled by remember { - mutableStateOf(scopeRules.any { it.subject == feature.key }) - } - val featureEnabledText = if (featureEnabled) "Enabled" else "Disabled" - Row { - Text(text = "${feature.key}: $featureEnabledText", maxLines = 1) - Switch(checked = featureEnabled, onCheckedChange = { - context.modDatabase.toggleRuleFor(scope, id, feature.key, it) - featureEnabled = it - }) + ContentCard { + //manager anti features etc + MessagingRuleType.values().forEach { feature -> + var featureEnabled by remember { + mutableStateOf(scopeRules.any { it.subject == feature.key }) + } + val featureEnabledText = if (featureEnabled) "Enabled" else "Disabled" + + Row { + Text(text = "${feature.key}: $featureEnabledText", maxLines = 1) + Switch(checked = featureEnabled, onCheckedChange = { + context.modDatabase.toggleRuleFor(scope, id, feature.key, it) + featureEnabled = it + }) + } } } } } @Composable + private fun ContentCard(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Card( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth() + .then(modifier) + ) { + content() + } + } + } + + @Composable + private fun SectionTitle(title: String) { + Text( + text = title, + maxLines = 1, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier + .offset(x = 20.dp) + .padding(bottom = 10.dp) + ) + } + + //need to display all units? + private fun computeStreakETA(timestamp: Long): String { + val now = System.currentTimeMillis() + val stringBuilder = StringBuilder() + val diff = timestamp - now + val seconds = diff / 1000 + val minutes = seconds / 60 + val hours = minutes / 60 + val days = hours / 24 + if (days > 0) { + stringBuilder.append("$days days ") + return stringBuilder.toString() + } + if (hours > 0) { + stringBuilder.append("$hours hours ") + return stringBuilder.toString() + } + if (minutes > 0) { + stringBuilder.append("$minutes minutes ") + return stringBuilder.toString() + } + if (seconds > 0) { + stringBuilder.append("$seconds seconds ") + return stringBuilder.toString() + } + return "Expired" + } + + @Composable private fun Friend() { //fetch the friend from the database val friend = remember { context.modDatabase.getFriendInfo(id) } ?: run { Text(text = "Friend not found") return } - Column { - Text(text = friend.displayName ?: "No display name", maxLines = 1) - Text(text = "bitmojiId: ${friend.bitmojiId ?: "No bitmojiId"}", maxLines = 1) - Text(text = "selfieId: ${friend.selfieId ?: "No selfieId"}", maxLines = 1) + val streaks = remember { + context.modDatabase.getFriendStreaks(id) + } + + Column( + modifier = Modifier + .padding(10.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) -> + if (selfieId == null || bitmojiId == null) return@let null + BitmojiSelfie.getBitmojiSelfie( + selfieId, + bitmojiId, + BitmojiSelfie.BitmojiSelfieType.THREE_D + ) + } + BitmojiImage(context = context, url = bitmojiUrl, size = 100) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = friend.displayName ?: friend.mutableUsername, + maxLines = 1, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = friend.mutableUsername, + maxLines = 1, + fontSize = 12.sp, + fontWeight = FontWeight.Light + ) Spacer(modifier = Modifier.height(16.dp)) + DeleteScopeEntityButton() } + Spacer(modifier = Modifier.height(16.dp)) + Column { + //streaks + streaks?.let { + var shouldNotify by remember { mutableStateOf(it.notify) } + SectionTitle("Streaks") + ContentCard { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f), + ) { + Text(text = "Count: ${streaks.length}", maxLines = 1) + Text(text = "Expires in: ${computeStreakETA(streaks.expirationTimestamp)}", maxLines = 1) + } + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "Notify Expiration", maxLines = 1) + Switch(checked = shouldNotify, onCheckedChange = { + context.modDatabase.setFriendStreaksNotify(id, it) + shouldNotify = it + }) + } + } + } + } + } } @Composable diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/SocialSection.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -147,7 +146,7 @@ class SocialSection : Section() { ) { val bitmojiUrl = (friend.selfieId to friend.bitmojiId).let { (selfieId, bitmojiId) -> if (selfieId == null || bitmojiId == null) return@let null - BitmojiSelfie.getBitmojiSelfie(selfieId, bitmojiId, BitmojiSelfie.BitmojiSelfieType.STANDARD) + BitmojiSelfie.getBitmojiSelfie(selfieId, bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D) } BitmojiImage(context = context, url = bitmojiUrl) Column( diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/SyncCallback.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/SyncCallback.aidl @@ -6,12 +6,12 @@ interface SyncCallback { * @param uuid The uuid of the friend to sync * @return The serialized friend data */ - String syncFriend(String uuid); + @nullable String syncFriend(String uuid); /** * Called when the conversation data has been synced * @param uuid The uuid of the conversation to sync * @return The serialized conversation data */ - String syncGroup(String uuid); + @nullable String syncGroup(String uuid); } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -89,6 +89,7 @@ class SnapEnhance { Logger.debug("Reloading config") appContext.reloadConfig() + syncRemote() } } @@ -131,6 +132,7 @@ class SnapEnhance { val database = appContext.database appContext.executeAsync { + Logger.debug("request remote sync") appContext.bridgeClient.sync(object : SyncCallback.Stub() { override fun syncFriend(uuid: String): String? { return database.getFriendInfo(uuid)?.toJson() diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt @@ -29,7 +29,7 @@ data class FriendStreaks( val userId: String, val notify: Boolean, val expirationTimestamp: Long, - val count: Int + val length: Int ) : SerializableDataObject()