mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 13:47:47 +02:00
refactor(app): navigation
This commit is contained in:
@ -15,15 +15,9 @@ import me.rhunk.snapenhance.SharedContextHolder
|
|||||||
import me.rhunk.snapenhance.common.ui.AppMaterialTheme
|
import me.rhunk.snapenhance.common.ui.AppMaterialTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var sections: Map<EnumSection, Section>
|
|
||||||
private lateinit var navController: NavHostController
|
private lateinit var navController: NavHostController
|
||||||
private lateinit var managerContext: RemoteSideContext
|
private lateinit var managerContext: RemoteSideContext
|
||||||
|
|
||||||
override fun onPostResume() {
|
|
||||||
super.onPostResume()
|
|
||||||
sections.values.forEach { it.onResumed() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if (::navController.isInitialized.not()) return
|
if (::navController.isInitialized.not()) return
|
||||||
@ -40,37 +34,30 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME
|
|
||||||
managerContext = SharedContextHolder.remote(this).apply {
|
managerContext = SharedContextHolder.remote(this).apply {
|
||||||
activity = this@MainActivity
|
activity = this@MainActivity
|
||||||
checkForRequirements()
|
checkForRequirements()
|
||||||
}
|
}
|
||||||
|
|
||||||
sections = EnumSection.entries.associateWith {
|
val routes = Routes(managerContext)
|
||||||
it.section.java.constructors.first().newInstance() as Section
|
routes.getRoutes().forEach { it.init() }
|
||||||
}.onEach { (section, instance) ->
|
|
||||||
with(instance) {
|
|
||||||
enumSection = section
|
|
||||||
context = managerContext
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
navController = rememberNavController()
|
navController = rememberNavController()
|
||||||
val navigation = remember { Navigation(managerContext, sections, navController) }
|
val navigation = remember {
|
||||||
|
Navigation(managerContext, navController, routes.also {
|
||||||
|
it.navController = navController
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val startDestination = remember { intent.getStringExtra("route") ?: routes.home.routeInfo.id }
|
||||||
|
|
||||||
AppMaterialTheme {
|
AppMaterialTheme {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
topBar = { navigation.TopBar() },
|
topBar = { navigation.TopBar() },
|
||||||
bottomBar = { navigation.NavBar() },
|
bottomBar = { navigation.BottomBar() },
|
||||||
floatingActionButton = { navigation.Fab() }
|
floatingActionButton = { navigation.FloatingActionButton() }
|
||||||
) { innerPadding ->
|
) { innerPadding -> navigation.Content(innerPadding, startDestination) }
|
||||||
navigation.NavigationHost(
|
|
||||||
innerPadding = innerPadding,
|
|
||||||
startDestination = startDestination
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,64 +10,40 @@ import androidx.compose.material.icons.filled.ArrowBack
|
|||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.lerp
|
import androidx.compose.ui.unit.lerp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavDestination
|
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import androidx.navigation.navigation
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class Navigation(
|
class Navigation(
|
||||||
private val context: RemoteSideContext,
|
private val context: RemoteSideContext,
|
||||||
private val sections: Map<EnumSection, Section>,
|
private val navController: NavHostController,
|
||||||
private val navHostController: NavHostController
|
val routes: Routes = Routes(context).also {
|
||||||
|
it.navController = navController
|
||||||
|
}
|
||||||
){
|
){
|
||||||
@Composable
|
|
||||||
fun NavigationHost(
|
|
||||||
startDestination: EnumSection,
|
|
||||||
innerPadding: PaddingValues
|
|
||||||
) {
|
|
||||||
NavHost(
|
|
||||||
navHostController,
|
|
||||||
startDestination = startDestination.route,
|
|
||||||
Modifier.padding(innerPadding),
|
|
||||||
enterTransition = { fadeIn(tween(100)) },
|
|
||||||
exitTransition = { fadeOut(tween(100)) }
|
|
||||||
) {
|
|
||||||
sections.forEach { (_, instance) ->
|
|
||||||
instance.navController = navHostController
|
|
||||||
instance.build(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCurrentSection(navDestination: NavDestination) = sections.firstNotNullOf { (section, instance) ->
|
|
||||||
if (navDestination.hierarchy.any { it.route == section.route }) {
|
|
||||||
instance
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBar() {
|
fun TopBar() {
|
||||||
val navBackStackEntry by navHostController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentDestination = navBackStackEntry?.destination ?: return
|
|
||||||
val currentSection = getCurrentSection(currentDestination)
|
val canGoBack = remember (navBackStackEntry) { routes.getCurrentRoute(navBackStackEntry)?.let {
|
||||||
|
!it.routeInfo.primary || it.routeInfo.childIds.contains(routes.currentDestination)
|
||||||
|
} == true }
|
||||||
|
|
||||||
TopAppBar(title = {
|
TopAppBar(title = {
|
||||||
currentSection.Title()
|
routes.getCurrentRoute(navBackStackEntry)?.title?.invoke() ?: Text(text = routes.getCurrentRoute(navBackStackEntry)?.routeInfo?.translatedKey ?: "Unknown Page")
|
||||||
}, navigationIcon = {
|
}, navigationIcon = {
|
||||||
val backButtonAnimation by animateFloatAsState(if (currentSection.canGoBack()) 1f else 0f,
|
val backButtonAnimation by animateFloatAsState(if (canGoBack) 1f else 0f,
|
||||||
label = "backButtonAnimation"
|
label = "backButtonAnimation"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,65 +54,88 @@ class Navigation(
|
|||||||
.height(48.dp)
|
.height(48.dp)
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { navHostController.popBackStack() }
|
onClick = {
|
||||||
|
if (canGoBack) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.ArrowBack, contentDescription = null)
|
Icon(Icons.Filled.ArrowBack, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, actions = {
|
}, actions = {
|
||||||
currentSection.TopBarActions(this)
|
routes.getCurrentRoute(navBackStackEntry)?.topBarActions?.invoke(this)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Fab() {
|
fun BottomBar() {
|
||||||
val navBackStackEntry by navHostController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentDestination = navBackStackEntry?.destination ?: return
|
val primaryRoutes = remember { routes.getRoutes().filter { it.routeInfo.primary } }
|
||||||
val currentSection = getCurrentSection(currentDestination)
|
|
||||||
|
|
||||||
currentSection.FloatingActionButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NavBar() {
|
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
val navBackStackEntry by navHostController.currentBackStackEntryAsState()
|
val currentRoute = routes.getCurrentRoute(navBackStackEntry)
|
||||||
val currentDestination = navBackStackEntry?.destination
|
primaryRoutes.forEach { route ->
|
||||||
sections.keys.forEach { section ->
|
|
||||||
fun selected() = currentDestination?.hierarchy?.any { it.route == section.route } == true
|
|
||||||
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
alwaysShowLabel = false,
|
alwaysShowLabel = false,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxHeight(),
|
||||||
.fillMaxHeight(),
|
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(imageVector = route.routeInfo.icon, contentDescription = null)
|
||||||
imageVector = section.icon,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
modifier = Modifier.wrapContentWidth(unbounded = true),
|
modifier = Modifier.wrapContentWidth(unbounded = true),
|
||||||
text = if (selected()) context.translation["manager.routes.${section.route}"] else "",
|
text = if (currentRoute == route) context.translation["manager.routes.${route.routeInfo.key.substringBefore("/")}"] else "",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selected = selected(),
|
selected = currentRoute == route,
|
||||||
onClick = {
|
onClick = {
|
||||||
navHostController.navigate(section.route) {
|
route.navigateReset()
|
||||||
popUpTo(navHostController.graph.findStartDestination().id) {
|
|
||||||
saveState = true
|
|
||||||
}
|
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FloatingActionButton() {
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
routes.getCurrentRoute(navBackStackEntry)?.floatingActionButton?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Content(paddingValues: PaddingValues, startDestination: String) {
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = startDestination,
|
||||||
|
Modifier.padding(paddingValues),
|
||||||
|
enterTransition = { fadeIn(tween(100)) },
|
||||||
|
exitTransition = { fadeOut(tween(100)) }
|
||||||
|
) {
|
||||||
|
routes.getRoutes().filter { it.parentRoute == null }.forEach { route ->
|
||||||
|
val children = routes.getRoutes().filter { it.parentRoute == route }
|
||||||
|
if (children.isEmpty()) {
|
||||||
|
composable(route.routeInfo.id) {
|
||||||
|
route.content.invoke(it)
|
||||||
|
}
|
||||||
|
route.customComposables.invoke(this)
|
||||||
|
} else {
|
||||||
|
navigation("main_" + route.routeInfo.id, route.routeInfo.id) {
|
||||||
|
composable("main_" + route.routeInfo.id) {
|
||||||
|
route.content.invoke(it)
|
||||||
|
}
|
||||||
|
children.forEach { child ->
|
||||||
|
composable(child.routeInfo.id) {
|
||||||
|
child.content.invoke(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
route.customComposables.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
134
app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt
Normal file
134
app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Routes.kt
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.manager
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.DataObject
|
||||||
|
import androidx.compose.material.icons.filled.Group
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Stars
|
||||||
|
import androidx.compose.material.icons.filled.TaskAlt
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
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.TasksRoot
|
||||||
|
import me.rhunk.snapenhance.ui.manager.pages.features.FeaturesRoot
|
||||||
|
import me.rhunk.snapenhance.ui.manager.pages.home.HomeLogs
|
||||||
|
import me.rhunk.snapenhance.ui.manager.pages.home.HomeRoot
|
||||||
|
import me.rhunk.snapenhance.ui.manager.pages.home.HomeSettings
|
||||||
|
import me.rhunk.snapenhance.ui.manager.pages.scripting.ScriptingRoot
|
||||||
|
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.SocialRoot
|
||||||
|
|
||||||
|
|
||||||
|
data class RouteInfo(
|
||||||
|
val id: String,
|
||||||
|
val key: String = id,
|
||||||
|
val icon: ImageVector = Icons.Default.Home,
|
||||||
|
val primary: Boolean = false,
|
||||||
|
) {
|
||||||
|
var translatedKey: String? = null
|
||||||
|
val childIds = mutableListOf<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||||
|
class Routes(
|
||||||
|
private val context: RemoteSideContext,
|
||||||
|
) {
|
||||||
|
lateinit var navController: NavController
|
||||||
|
private val routes = mutableListOf<Route>()
|
||||||
|
|
||||||
|
val tasks = route(RouteInfo("tasks", icon = Icons.Default.TaskAlt, primary = true), TasksRoot())
|
||||||
|
|
||||||
|
val features = route(RouteInfo("features", icon = Icons.Default.Stars, primary = true), FeaturesRoot())
|
||||||
|
|
||||||
|
val home = route(RouteInfo("home", icon = Icons.Default.Home, primary = true), HomeRoot())
|
||||||
|
val settings = route(RouteInfo("home_settings"), HomeSettings()).parent(home)
|
||||||
|
val homeLogs = route(RouteInfo("home_logs"), HomeLogs()).parent(home)
|
||||||
|
|
||||||
|
val social = route(RouteInfo("social", icon = Icons.Default.Group, primary = true), SocialRoot())
|
||||||
|
val manageScope = route(RouteInfo("manage_scope/?scope={scope}&id={id}"), ManageScope()).parent(social)
|
||||||
|
val messagingPreview = route(RouteInfo("messaging_preview/?scope={scope}&id={id}"), MessagingPreview()).parent(social)
|
||||||
|
val loggedStories = route(RouteInfo("logged_stories/?id={id}"), LoggedStories()).parent(social)
|
||||||
|
|
||||||
|
val scripting = route(RouteInfo("scripts", icon = Icons.Filled.DataObject, primary = true), ScriptingRoot())
|
||||||
|
|
||||||
|
open class Route {
|
||||||
|
open val init: () -> Unit = { }
|
||||||
|
open val title: @Composable (() -> Unit)? = null
|
||||||
|
open val topBarActions: @Composable RowScope.() -> Unit = {}
|
||||||
|
open val floatingActionButton: @Composable () -> Unit = {}
|
||||||
|
open val content: @Composable (NavBackStackEntry) -> Unit = {}
|
||||||
|
open val customComposables: NavGraphBuilder.() -> Unit = {}
|
||||||
|
|
||||||
|
var parentRoute: Route? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
lateinit var context: RemoteSideContext
|
||||||
|
lateinit var routeInfo: RouteInfo
|
||||||
|
lateinit var routes: Routes
|
||||||
|
|
||||||
|
private fun replaceArguments(id: String, args: Map<String, String>) = args.takeIf { it.isNotEmpty() }?.let {
|
||||||
|
args.entries.fold(id) { acc, (key, value) ->
|
||||||
|
acc.replace("{$key}", value)
|
||||||
|
}
|
||||||
|
} ?: id
|
||||||
|
|
||||||
|
fun navigate(args: MutableMap<String, String>.() -> Unit = {}) {
|
||||||
|
routes.navController.navigate(replaceArguments(routeInfo.id, HashMap<String, String>().apply { args() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateReset(args: MutableMap<String, String>.() -> Unit = {}) {
|
||||||
|
routes.navController.navigate(replaceArguments(routeInfo.id, HashMap<String, String>().apply { args() })) {
|
||||||
|
popUpTo(routes.navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parent(route: Route): Route {
|
||||||
|
assert(route.routeInfo.primary) { "Parent route must be a primary route" }
|
||||||
|
parentRoute = route
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentRoute: Route?
|
||||||
|
get() = routes.firstOrNull { route ->
|
||||||
|
navController.currentBackStackEntry?.destination?.hierarchy?.any { it.route == route.routeInfo.id } ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentDestination: String?
|
||||||
|
get() = navController.currentBackStackEntry?.destination?.route
|
||||||
|
|
||||||
|
fun getCurrentRoute(navBackStackEntry: NavBackStackEntry?): Route? {
|
||||||
|
if (navBackStackEntry == null) return null
|
||||||
|
|
||||||
|
return navBackStackEntry.destination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||||
|
routes.firstOrNull { route ->
|
||||||
|
route.routeInfo.id == destination.route || route.routeInfo.childIds.contains(destination.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRoutes(): List<Route> = routes
|
||||||
|
|
||||||
|
private fun route(routeInfo: RouteInfo, route: Route): Route {
|
||||||
|
route.apply {
|
||||||
|
this.routeInfo = routeInfo
|
||||||
|
routes = this@Routes
|
||||||
|
context = this@Routes.context
|
||||||
|
this.routeInfo.translatedKey = context.translation.getOrNull("manager.routes.${route.routeInfo.key.substringBefore("/")}")
|
||||||
|
}
|
||||||
|
routes.add(route)
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
}
|
@ -1,91 +0,0 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
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.RemoteSideContext
|
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
|
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.TasksSection
|
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
|
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.home.HomeSection
|
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.scripting.ScriptsSection
|
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
enum class EnumSection(
|
|
||||||
val route: String,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val section: KClass<out Section> = NotImplemented::class
|
|
||||||
) {
|
|
||||||
TASKS(
|
|
||||||
route = "tasks",
|
|
||||||
icon = Icons.Filled.TaskAlt,
|
|
||||||
section = TasksSection::class
|
|
||||||
),
|
|
||||||
FEATURES(
|
|
||||||
route = "features",
|
|
||||||
icon = Icons.Filled.Stars,
|
|
||||||
section = FeaturesSection::class
|
|
||||||
),
|
|
||||||
HOME(
|
|
||||||
route = "home",
|
|
||||||
icon = Icons.Filled.Home,
|
|
||||||
section = HomeSection::class
|
|
||||||
),
|
|
||||||
SOCIAL(
|
|
||||||
route = "social",
|
|
||||||
icon = Icons.Filled.Group,
|
|
||||||
section = SocialSection::class
|
|
||||||
),
|
|
||||||
SCRIPTS(
|
|
||||||
route = "scripts",
|
|
||||||
icon = Icons.Filled.DataObject,
|
|
||||||
section = ScriptsSection::class
|
|
||||||
);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromRoute(route: String): EnumSection {
|
|
||||||
return entries.first { it.route == route }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
open class Section {
|
|
||||||
lateinit var enumSection: EnumSection
|
|
||||||
lateinit var context: RemoteSideContext
|
|
||||||
lateinit var navController: NavController
|
|
||||||
|
|
||||||
val currentRoute get() = navController.currentBackStackEntry?.destination?.route
|
|
||||||
|
|
||||||
open fun init() {}
|
|
||||||
open fun onResumed() {}
|
|
||||||
|
|
||||||
open fun sectionTopBarName(): String = context.translation["manager.routes.${enumSection.route}"]
|
|
||||||
open fun canGoBack(): Boolean = false
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
open fun Title() { Text(text = sectionTopBarName()) }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
open fun Content() { NotImplemented() }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
open fun TopBarActions(rowScope: RowScope) {}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
open fun FloatingActionButton() {}
|
|
||||||
|
|
||||||
open fun build(navGraphBuilder: NavGraphBuilder) {
|
|
||||||
navGraphBuilder.composable(enumSection.route) {
|
|
||||||
Content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections
|
package me.rhunk.snapenhance.ui.manager.pages
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@ -15,12 +15,12 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.rhunk.snapenhance.bridge.DownloadCallback
|
import me.rhunk.snapenhance.bridge.DownloadCallback
|
||||||
@ -35,13 +35,13 @@ import me.rhunk.snapenhance.task.PendingTaskListener
|
|||||||
import me.rhunk.snapenhance.task.Task
|
import me.rhunk.snapenhance.task.Task
|
||||||
import me.rhunk.snapenhance.task.TaskStatus
|
import me.rhunk.snapenhance.task.TaskStatus
|
||||||
import me.rhunk.snapenhance.task.TaskType
|
import me.rhunk.snapenhance.task.TaskType
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.OnLifecycleEvent
|
import me.rhunk.snapenhance.ui.util.OnLifecycleEvent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class TasksSection : Section() {
|
class TasksRoot : Routes.Route() {
|
||||||
private var activeTasks by mutableStateOf(listOf<PendingTask>())
|
private var activeTasks by mutableStateOf(listOf<PendingTask>())
|
||||||
private lateinit var recentTasks: MutableList<Task>
|
private lateinit var recentTasks: MutableList<Task>
|
||||||
private val taskSelection = mutableStateListOf<Pair<Task, DocumentFile?>>()
|
private val taskSelection = mutableStateListOf<Pair<Task, DocumentFile?>>()
|
||||||
@ -132,9 +132,7 @@ class TasksSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val topBarActions: @Composable() (RowScope.() -> Unit) = {
|
||||||
@Composable
|
|
||||||
override fun TopBarActions(rowScope: RowScope) {
|
|
||||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (taskSelection.size == 1 && taskSelection.firstOrNull()?.second?.exists() == true) {
|
if (taskSelection.size == 1 && taskSelection.firstOrNull()?.second?.exists() == true) {
|
||||||
@ -386,9 +384,7 @@ class TasksSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
@Composable
|
|
||||||
override fun Content() {
|
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
recentTasks = remember { mutableStateListOf() }
|
recentTasks = remember { mutableStateListOf() }
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.features
|
package me.rhunk.snapenhance.ui.manager.pages.features
|
||||||
|
|
||||||
typealias ClickCallback = (Boolean) -> Unit
|
typealias ClickCallback = (Boolean) -> Unit
|
||||||
typealias RegisterClickCallback = (ClickCallback) -> ClickCallback
|
typealias RegisterClickCallback = (ClickCallback) -> ClickCallback
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.features
|
package me.rhunk.snapenhance.ui.manager.pages.features
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -30,33 +30,29 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navigation
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.rhunk.snapenhance.common.config.*
|
import me.rhunk.snapenhance.common.config.*
|
||||||
import me.rhunk.snapenhance.ui.manager.MainActivity
|
import me.rhunk.snapenhance.ui.manager.MainActivity
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.*
|
import me.rhunk.snapenhance.ui.util.*
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class FeaturesSection : Section() {
|
class FeaturesRoot : Routes.Route() {
|
||||||
private val alertDialogs by lazy { AlertDialogs(context.translation) }
|
private val alertDialogs by lazy { AlertDialogs(context.translation) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAIN_ROUTE = "feature_root"
|
|
||||||
const val FEATURE_CONTAINER_ROUTE = "feature_container/{name}"
|
const val FEATURE_CONTAINER_ROUTE = "feature_container/{name}"
|
||||||
const val SEARCH_FEATURE_ROUTE = "search_feature/{keyword}"
|
const val SEARCH_FEATURE_ROUTE = "search_feature/{keyword}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private var activityLauncherHelper: ActivityLauncherHelper? = null
|
private var activityLauncherHelper: ActivityLauncherHelper? = null
|
||||||
private val featuresRouteName by lazy { context.translation["manager.routes.features"] }
|
|
||||||
|
|
||||||
private lateinit var rememberScaffoldState: BottomSheetScaffoldState
|
private lateinit var rememberScaffoldState: BottomSheetScaffoldState
|
||||||
|
|
||||||
private val allContainers by lazy {
|
private val allContainers by lazy {
|
||||||
@ -85,24 +81,14 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToMainRoot() {
|
private fun navigateToMainRoot() {
|
||||||
navController.navigate(MAIN_ROUTE, NavOptions.Builder()
|
routes.navController.navigate(routeInfo.id, NavOptions.Builder()
|
||||||
.setPopUpTo(navController.graph.findStartDestination().id, false)
|
.setPopUpTo(routes.navController.graph.findStartDestination().id, false)
|
||||||
.setLaunchSingleTop(true)
|
.setLaunchSingleTop(true)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canGoBack() = sectionTopBarName() != featuresRouteName
|
override val init: () -> Unit = {
|
||||||
|
|
||||||
override fun sectionTopBarName(): String {
|
|
||||||
navController.currentBackStackEntry?.arguments?.getString("name")?.let { routeName ->
|
|
||||||
val currentContainerPair = allContainers[routeName]
|
|
||||||
return context.translation["${currentContainerPair?.key?.propertyTranslationPath()}.name"]
|
|
||||||
}
|
|
||||||
return featuresRouteName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun init() {
|
|
||||||
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,39 +97,39 @@ class FeaturesSection : Section() {
|
|||||||
//open manager if activity launcher is null
|
//open manager if activity launcher is null
|
||||||
val intent = Intent(context.androidContext, MainActivity::class.java)
|
val intent = Intent(context.androidContext, MainActivity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
intent.putExtra("route", enumSection.route)
|
intent.putExtra("route", routeInfo.id)
|
||||||
context.androidContext.startActivity(intent)
|
context.androidContext.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun build(navGraphBuilder: NavGraphBuilder) {
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
navGraphBuilder.navigation(route = enumSection.route, startDestination = MAIN_ROUTE) {
|
Container(context.config.root)
|
||||||
composable(MAIN_ROUTE) {
|
}
|
||||||
Container(context.config.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(FEATURE_CONTAINER_ROUTE, enterTransition = {
|
override val customComposables: NavGraphBuilder.() -> Unit = {
|
||||||
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(100))
|
routeInfo.childIds.addAll(listOf(FEATURE_CONTAINER_ROUTE, SEARCH_FEATURE_ROUTE))
|
||||||
}, exitTransition = {
|
|
||||||
slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(300))
|
composable(FEATURE_CONTAINER_ROUTE, enterTransition = {
|
||||||
}) { backStackEntry ->
|
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left, animationSpec = tween(100))
|
||||||
backStackEntry.arguments?.getString("name")?.let { containerName ->
|
}, exitTransition = {
|
||||||
allContainers[containerName]?.let {
|
slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Right, animationSpec = tween(300))
|
||||||
Container(it.value.get() as ConfigContainer)
|
}) { backStackEntry ->
|
||||||
}
|
backStackEntry.arguments?.getString("name")?.let { containerName ->
|
||||||
|
allContainers[containerName]?.let {
|
||||||
|
Container(it.value.get() as ConfigContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
composable(SEARCH_FEATURE_ROUTE) { backStackEntry ->
|
composable(SEARCH_FEATURE_ROUTE) { backStackEntry ->
|
||||||
backStackEntry.arguments?.getString("keyword")?.let { keyword ->
|
backStackEntry.arguments?.getString("keyword")?.let { keyword ->
|
||||||
val properties = allProperties.filter {
|
val properties = allProperties.filter {
|
||||||
it.key.name.contains(keyword, ignoreCase = true) ||
|
it.key.name.contains(keyword, ignoreCase = true) ||
|
||||||
context.translation[it.key.propertyName()].contains(keyword, ignoreCase = true) ||
|
context.translation[it.key.propertyName()].contains(keyword, ignoreCase = true) ||
|
||||||
context.translation[it.key.propertyDescription()].contains(keyword, ignoreCase = true)
|
context.translation[it.key.propertyDescription()].contains(keyword, ignoreCase = true)
|
||||||
}.map { PropertyPair(it.key, it.value) }
|
}.map { PropertyPair(it.key, it.value) }
|
||||||
|
|
||||||
PropertiesView(properties)
|
PropertiesView(properties)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,7 +248,7 @@ class FeaturesSection : Section() {
|
|||||||
val container = propertyValue.get() as ConfigContainer
|
val container = propertyValue.get() as ConfigContainer
|
||||||
|
|
||||||
registerClickCallback {
|
registerClickCallback {
|
||||||
navController.navigate(FEATURE_CONTAINER_ROUTE.replace("{name}", property.name))
|
routes.navController.navigate(FEATURE_CONTAINER_ROUTE.replace("{name}", property.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!container.hasGlobalState) return
|
if (!container.hasGlobalState) return
|
||||||
@ -398,10 +384,10 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
currentSearchJob?.cancel()
|
currentSearchJob?.cancel()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(300)
|
delay(150)
|
||||||
navController.navigate(SEARCH_FEATURE_ROUTE.replace("{keyword}", keyword), NavOptions.Builder()
|
routes.navController.navigate(SEARCH_FEATURE_ROUTE.replace("{keyword}", keyword), NavOptions.Builder()
|
||||||
.setLaunchSingleTop(true)
|
.setLaunchSingleTop(true)
|
||||||
.setPopUpTo(MAIN_ROUTE, false)
|
.setPopUpTo(routeInfo.id, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}.also { currentSearchJob = it }
|
}.also { currentSearchJob = it }
|
||||||
@ -428,13 +414,12 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val topBarActions: @Composable (RowScope.() -> Unit) = topBarActions@{
|
||||||
override fun TopBarActions(rowScope: RowScope) {
|
|
||||||
var showSearchBar by remember { mutableStateOf(false) }
|
var showSearchBar by remember { mutableStateOf(false) }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
if (showSearchBar) {
|
if (showSearchBar) {
|
||||||
FeatureSearchBar(rowScope, focusRequester)
|
FeatureSearchBar(this, focusRequester)
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
@ -442,18 +427,18 @@ class FeaturesSection : Section() {
|
|||||||
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
showSearchBar = showSearchBar.not()
|
showSearchBar = showSearchBar.not()
|
||||||
if (!showSearchBar && currentRoute == SEARCH_FEATURE_ROUTE) {
|
if (!showSearchBar && routes.currentDestination == SEARCH_FEATURE_ROUTE) {
|
||||||
navigateToMainRoot()
|
navigateToMainRoot()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (showSearchBar) Icons.Filled.Close
|
imageVector = if (showSearchBar) Icons.Filled.Close
|
||||||
else Icons.Filled.Search,
|
else Icons.Filled.Search,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSearchBar) return
|
if (showSearchBar) return@topBarActions
|
||||||
|
|
||||||
var showExportDropdownMenu by remember { mutableStateOf(false) }
|
var showExportDropdownMenu by remember { mutableStateOf(false) }
|
||||||
var showResetConfirmationDialog by remember { mutableStateOf(false) }
|
var showResetConfirmationDialog by remember { mutableStateOf(false) }
|
||||||
@ -504,11 +489,13 @@ class FeaturesSection : Section() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = { showExportDropdownMenu = !showExportDropdownMenu}) {
|
if (context.activity != null) {
|
||||||
Icon(
|
IconButton(onClick = { showExportDropdownMenu = !showExportDropdownMenu}) {
|
||||||
imageVector = Icons.Filled.MoreVert,
|
Icon(
|
||||||
contentDescription = null
|
imageVector = Icons.Filled.MoreVert,
|
||||||
)
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showExportDropdownMenu) {
|
if (showExportDropdownMenu) {
|
||||||
@ -553,8 +540,7 @@ class FeaturesSection : Section() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val floatingActionButton: @Composable () -> Unit = {
|
||||||
override fun FloatingActionButton() {
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.home
|
package me.rhunk.snapenhance.ui.manager.pages.home
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
@ -27,27 +27,74 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavBackStackEntry
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.rhunk.snapenhance.LogReader
|
import me.rhunk.snapenhance.LogReader
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
|
||||||
import me.rhunk.snapenhance.common.logger.LogChannel
|
import me.rhunk.snapenhance.common.logger.LogChannel
|
||||||
import me.rhunk.snapenhance.common.logger.LogLevel
|
import me.rhunk.snapenhance.common.logger.LogLevel
|
||||||
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
||||||
import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator
|
import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator
|
||||||
import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState
|
import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState
|
||||||
import me.rhunk.snapenhance.ui.util.saveFile
|
import me.rhunk.snapenhance.ui.util.saveFile
|
||||||
|
|
||||||
class HomeSubSection(
|
class HomeLogs : Routes.Route() {
|
||||||
private val context: RemoteSideContext
|
|
||||||
) {
|
|
||||||
private val logListState by lazy { LazyListState(0) }
|
private val logListState by lazy { LazyListState(0) }
|
||||||
|
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
||||||
|
|
||||||
@Composable
|
override val init: () -> Unit = {
|
||||||
fun LogsSection() {
|
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val topBarActions: @Composable (RowScope.() -> Unit) = {
|
||||||
|
var showDropDown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
showDropDown = true
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Filled.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showDropDown,
|
||||||
|
onDismissRequest = { showDropDown = false },
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
context.log.clearLogs()
|
||||||
|
navigate()
|
||||||
|
showDropDown = false
|
||||||
|
}, text = {
|
||||||
|
Text(
|
||||||
|
text = context.translation["manager.sections.home.logs.clear_logs_button"]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
activityLauncherHelper.saveFile("snapenhance-logs-${System.currentTimeMillis()}.zip", "application/zip") { uri ->
|
||||||
|
context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
|
||||||
|
runCatching {
|
||||||
|
context.log.exportLogsToZip(it)
|
||||||
|
context.longToast("Saved logs to $uri")
|
||||||
|
}.onFailure {
|
||||||
|
context.longToast("Failed to save logs to $uri!")
|
||||||
|
context.log.error("Failed to save logs to $uri!", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showDropDown = false
|
||||||
|
}, text = {
|
||||||
|
Text(
|
||||||
|
text = context.translation["manager.sections.home.logs.export_logs_button"]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
var lineCount by remember { mutableIntStateOf(0) }
|
var lineCount by remember { mutableIntStateOf(0) }
|
||||||
@ -172,56 +219,7 @@ class HomeSubSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val floatingActionButton: @Composable () -> Unit = {
|
||||||
fun LogsTopBarButtons(activityLauncherHelper: ActivityLauncherHelper, navController: NavController, rowScope: RowScope) {
|
|
||||||
var showDropDown by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
IconButton(onClick = {
|
|
||||||
showDropDown = true
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Filled.MoreVert, contentDescription = null)
|
|
||||||
}
|
|
||||||
|
|
||||||
rowScope.apply {
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showDropDown,
|
|
||||||
onDismissRequest = { showDropDown = false },
|
|
||||||
modifier = Modifier.align(Alignment.CenterVertically)
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
context.log.clearLogs()
|
|
||||||
navController.navigate(HomeSection.LOGS_SECTION_ROUTE)
|
|
||||||
showDropDown = false
|
|
||||||
}, text = {
|
|
||||||
Text(
|
|
||||||
text = context.translation["manager.sections.home.logs.clear_logs_button"]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
activityLauncherHelper.saveFile("snapenhance-logs-${System.currentTimeMillis()}.zip", "application/zip") { uri ->
|
|
||||||
context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
|
|
||||||
runCatching {
|
|
||||||
context.log.exportLogsToZip(it)
|
|
||||||
context.longToast("Saved logs to $uri")
|
|
||||||
}.onFailure {
|
|
||||||
context.longToast("Failed to save logs to $uri!")
|
|
||||||
context.log.error("Failed to save logs to $uri!", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showDropDown = false
|
|
||||||
}, text = {
|
|
||||||
Text(
|
|
||||||
text = context.translation["manager.sections.home.logs.export_logs_button"]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LogsActionButtons() {
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(5.dp),
|
verticalArrangement = Arrangement.spacedBy(5.dp),
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.home
|
package me.rhunk.snapenhance.ui.manager.pages.home
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -11,8 +11,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.BugReport
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
@ -25,36 +24,29 @@ import androidx.compose.ui.text.font.Font
|
|||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.navigation
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.rhunk.snapenhance.R
|
import me.rhunk.snapenhance.R
|
||||||
import me.rhunk.snapenhance.common.BuildConfig
|
import me.rhunk.snapenhance.common.BuildConfig
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
||||||
import me.rhunk.snapenhance.ui.manager.data.Updater
|
import me.rhunk.snapenhance.ui.manager.data.Updater
|
||||||
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
||||||
import java.util.Locale
|
import me.rhunk.snapenhance.ui.util.OnLifecycleEvent
|
||||||
|
|
||||||
class HomeSection : Section() {
|
class HomeRoot : Routes.Route() {
|
||||||
companion object {
|
companion object {
|
||||||
val cardMargin = 10.dp
|
val cardMargin = 10.dp
|
||||||
const val HOME_ROOT = "home_root"
|
|
||||||
const val LOGS_SECTION_ROUTE = "home_logs"
|
|
||||||
const val SETTINGS_SECTION_ROUTE = "home_settings"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var installationSummary: InstallationSummary? = null
|
|
||||||
private var userLocale: String? = null
|
|
||||||
private val homeSubSection by lazy { HomeSubSection(context) }
|
|
||||||
private var latestUpdate: Updater.LatestRelease? = null
|
|
||||||
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
||||||
|
|
||||||
override fun init() {
|
override val init: () -> Unit = {
|
||||||
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,91 +101,28 @@ class HomeSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResumed() {
|
override val topBarActions: @Composable (RowScope.() -> Unit) = {
|
||||||
if (!context.mappings.isMappingsLoaded) {
|
IconButton(onClick = {
|
||||||
context.mappings.init(context.androidContext)
|
routes.homeLogs.navigate()
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Filled.BugReport, contentDescription = null)
|
||||||
}
|
}
|
||||||
context.coroutineScope.launch {
|
IconButton(onClick = {
|
||||||
userLocale = context.translation.loadedLocale.getDisplayName(Locale.getDefault())
|
routes.settings.navigate()
|
||||||
runCatching {
|
}) {
|
||||||
installationSummary = context.installationSummary
|
Icon(Icons.Filled.Settings, contentDescription = null)
|
||||||
}.onFailure {
|
|
||||||
context.longToast("SnapEnhance failed to load installation summary: ${it.message}")
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
if (!BuildConfig.DEBUG) {
|
|
||||||
latestUpdate = Updater.checkForLatestRelease()
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
context.longToast("SnapEnhance failed to check for updates: ${it.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sectionTopBarName(): String {
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
if (currentRoute == HOME_ROOT) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return context.translation["manager.routes.$currentRoute"]
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun FloatingActionButton() {
|
|
||||||
if (currentRoute == LOGS_SECTION_ROUTE) {
|
|
||||||
homeSubSection.LogsActionButtons()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun TopBarActions(rowScope: RowScope) {
|
|
||||||
rowScope.apply {
|
|
||||||
when (currentRoute) {
|
|
||||||
HOME_ROOT -> {
|
|
||||||
IconButton(onClick = {
|
|
||||||
navController.navigate(LOGS_SECTION_ROUTE)
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Filled.BugReport, contentDescription = null)
|
|
||||||
}
|
|
||||||
IconButton(onClick = {
|
|
||||||
navController.navigate(SETTINGS_SECTION_ROUTE)
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Filled.Settings, contentDescription = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOGS_SECTION_ROUTE -> {
|
|
||||||
homeSubSection.LogsTopBarButtons(activityLauncherHelper, navController, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun build(navGraphBuilder: NavGraphBuilder) {
|
|
||||||
navGraphBuilder.navigation(
|
|
||||||
route = enumSection.route,
|
|
||||||
startDestination = HOME_ROOT
|
|
||||||
) {
|
|
||||||
composable(HOME_ROOT) {
|
|
||||||
Content()
|
|
||||||
}
|
|
||||||
composable(LOGS_SECTION_ROUTE) {
|
|
||||||
homeSubSection.LogsSection()
|
|
||||||
}
|
|
||||||
composable(SETTINGS_SECTION_ROUTE) {
|
|
||||||
SettingsSection(activityLauncherHelper).also { it.context = context }.Content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview
|
|
||||||
override fun Content() {
|
|
||||||
val avenirNextFontFamily = remember {
|
val avenirNextFontFamily = remember {
|
||||||
FontFamily(
|
FontFamily(
|
||||||
Font(R.font.avenir_next_medium, FontWeight.Medium)
|
Font(R.font.avenir_next_medium, FontWeight.Medium)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var latestUpdate by remember { mutableStateOf<Updater.LatestRelease?>(null) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(ScrollState(0))
|
.verticalScroll(ScrollState(0))
|
||||||
@ -312,7 +241,37 @@ class HomeSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SummaryCards(installationSummary = installationSummary ?: return)
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
var installationSummary by remember { mutableStateOf(null as InstallationSummary?) }
|
||||||
|
|
||||||
|
fun updateInstallationSummary(scope: CoroutineScope) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
installationSummary = context.installationSummary
|
||||||
|
}.onFailure {
|
||||||
|
context.longToast("SnapEnhance failed to load installation summary: ${it.message}")
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
if (!BuildConfig.DEBUG) {
|
||||||
|
latestUpdate = Updater.checkForLatestRelease()
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
context.longToast("SnapEnhance failed to check for updates: ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLifecycleEvent { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
updateInstallationSummary(coroutineScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
updateInstallationSummary(coroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
installationSummary?.let { SummaryCards(installationSummary = it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.home
|
package me.rhunk.snapenhance.ui.manager.pages.home
|
||||||
|
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@ -15,22 +15,26 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import me.rhunk.snapenhance.common.Constants
|
import me.rhunk.snapenhance.common.Constants
|
||||||
import me.rhunk.snapenhance.common.action.EnumAction
|
import me.rhunk.snapenhance.common.action.EnumAction
|
||||||
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
|
import me.rhunk.snapenhance.common.bridge.types.BridgeFileType
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.setup.Requirements
|
import me.rhunk.snapenhance.ui.setup.Requirements
|
||||||
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
||||||
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
||||||
import me.rhunk.snapenhance.ui.util.saveFile
|
import me.rhunk.snapenhance.ui.util.saveFile
|
||||||
|
|
||||||
class SettingsSection(
|
class HomeSettings : Routes.Route() {
|
||||||
private val activityLauncherHelper: ActivityLauncherHelper
|
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
||||||
) : Section() {
|
|
||||||
private val dialogs by lazy { AlertDialogs(context.translation) }
|
private val dialogs by lazy { AlertDialogs(context.translation) }
|
||||||
|
|
||||||
|
override val init: () -> Unit = {
|
||||||
|
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RowTitle(title: String) {
|
private fun RowTitle(title: String) {
|
||||||
Text(text = title, modifier = Modifier.padding(16.dp), fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
Text(text = title, modifier = Modifier.padding(16.dp), fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||||
@ -102,10 +106,8 @@ class SettingsSection(
|
|||||||
) { content(this) }
|
) { content(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
override fun Content() {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -208,7 +210,7 @@ class SettingsSection(
|
|||||||
readOnly = true,
|
readOnly = true,
|
||||||
modifier = Modifier.menuAnchor()
|
modifier = Modifier.menuAnchor()
|
||||||
)
|
)
|
||||||
|
|
||||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
BridgeFileType.entries.forEach { fileType ->
|
BridgeFileType.entries.forEach { fileType ->
|
||||||
DropdownMenuItem(onClick = {
|
DropdownMenuItem(onClick = {
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.scripting
|
package me.rhunk.snapenhance.ui.manager.pages.scripting
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -25,17 +26,17 @@ import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
|||||||
import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface
|
import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface
|
||||||
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
|
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
|
||||||
import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface
|
import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
||||||
import me.rhunk.snapenhance.ui.util.chooseFolder
|
import me.rhunk.snapenhance.ui.util.chooseFolder
|
||||||
import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator
|
import me.rhunk.snapenhance.ui.util.pullrefresh.PullRefreshIndicator
|
||||||
import me.rhunk.snapenhance.ui.util.pullrefresh.pullRefresh
|
import me.rhunk.snapenhance.ui.util.pullrefresh.pullRefresh
|
||||||
import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState
|
import me.rhunk.snapenhance.ui.util.pullrefresh.rememberPullRefreshState
|
||||||
|
|
||||||
class ScriptsSection : Section() {
|
class ScriptingRoot : Routes.Route() {
|
||||||
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
||||||
|
|
||||||
override fun init() {
|
override val init: () -> Unit = {
|
||||||
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,8 +108,7 @@ class ScriptsSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val floatingActionButton: @Composable () -> Unit = {
|
||||||
override fun FloatingActionButton() {
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalAlignment = Alignment.End,
|
horizontalAlignment = Alignment.End,
|
||||||
@ -160,8 +160,7 @@ class ScriptsSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
override fun Content() {
|
|
||||||
var scriptModules by remember { mutableStateOf(listOf<ModuleInfo>()) }
|
var scriptModules by remember { mutableStateOf(listOf<ModuleInfo>()) }
|
||||||
var scriptingFolder by remember { mutableStateOf(null as DocumentFile?) }
|
var scriptingFolder by remember { mutableStateOf(null as DocumentFile?) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@ -289,17 +288,14 @@ class ScriptsSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val topBarActions: @Composable() (RowScope.() -> Unit) = {
|
||||||
override fun TopBarActions(rowScope: RowScope) {
|
IconButton(onClick = {
|
||||||
rowScope.apply {
|
context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||||
IconButton(onClick = {
|
data = "https://github.com/SnapEnhance/docs".toUri()
|
||||||
context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
data = "https://github.com/SnapEnhance/docs".toUri()
|
})
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
}) {
|
||||||
})
|
Icon(imageVector = Icons.Default.LibraryBooks, contentDescription = "Documentation")
|
||||||
}) {
|
|
||||||
Icon(imageVector = Icons.Default.LibraryBooks, contentDescription = "Documentation")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.social
|
package me.rhunk.snapenhance.ui.manager.pages.social
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@ -32,7 +32,7 @@ import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper
|
|||||||
|
|
||||||
class AddFriendDialog(
|
class AddFriendDialog(
|
||||||
private val context: RemoteSideContext,
|
private val context: RemoteSideContext,
|
||||||
private val section: SocialSection,
|
private val socialRoot: SocialRoot,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val translation by lazy { context.translation.getCategory("manager.dialogs.add_friend")}
|
private val translation by lazy { context.translation.getCategory("manager.dialogs.add_friend")}
|
||||||
@ -224,9 +224,7 @@ class AddFriendDialog(
|
|||||||
} else {
|
} else {
|
||||||
context.modDatabase.deleteGroup(group.conversationId)
|
context.modDatabase.deleteGroup(group.conversationId)
|
||||||
}
|
}
|
||||||
context.modDatabase.executeAsync {
|
socialRoot.updateScopeLists()
|
||||||
section.onResumed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,9 +249,7 @@ class AddFriendDialog(
|
|||||||
} else {
|
} else {
|
||||||
context.modDatabase.deleteFriend(friend.userId)
|
context.modDatabase.deleteFriend(friend.userId)
|
||||||
}
|
}
|
||||||
context.modDatabase.executeAsync {
|
socialRoot.updateScopeLists()
|
||||||
section.onResumed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,277 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.manager.pages.social
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import coil.annotation.ExperimentalCoilApi
|
||||||
|
import coil.disk.DiskCache
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import me.rhunk.snapenhance.bridge.DownloadCallback
|
||||||
|
import me.rhunk.snapenhance.common.data.FileType
|
||||||
|
import me.rhunk.snapenhance.common.data.StoryData
|
||||||
|
import me.rhunk.snapenhance.common.data.download.*
|
||||||
|
import me.rhunk.snapenhance.common.util.ktx.longHashCode
|
||||||
|
import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper
|
||||||
|
import me.rhunk.snapenhance.core.util.media.PreviewUtils
|
||||||
|
import me.rhunk.snapenhance.download.DownloadProcessor
|
||||||
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
|
import me.rhunk.snapenhance.ui.util.Dialog
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
class LoggedStories : Routes.Route() {
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
override val content: @Composable (NavBackStackEntry) -> Unit = content@{ navBackStackEntry ->
|
||||||
|
val userId = navBackStackEntry.arguments?.getString("id") ?: return@content
|
||||||
|
|
||||||
|
val stories = remember {
|
||||||
|
mutableStateListOf<StoryData>()
|
||||||
|
}
|
||||||
|
val friendInfo = remember {
|
||||||
|
context.modDatabase.getFriendInfo(userId)
|
||||||
|
}
|
||||||
|
val httpClient = remember { OkHttpClient() }
|
||||||
|
var lastStoryTimestamp by remember { mutableLongStateOf(Long.MAX_VALUE) }
|
||||||
|
|
||||||
|
var selectedStory by remember { mutableStateOf<StoryData?>(null) }
|
||||||
|
var coilCacheFile by remember { mutableStateOf<File?>(null) }
|
||||||
|
|
||||||
|
selectedStory?.let { story ->
|
||||||
|
Dialog(onDismissRequest = {
|
||||||
|
selectedStory = null
|
||||||
|
}) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(text = "Posted on ${story.postedAt.let {
|
||||||
|
DateFormat.getDateTimeInstance().format(Date(it))
|
||||||
|
}}")
|
||||||
|
Text(text = "Created at ${story.createdAt.let {
|
||||||
|
DateFormat.getDateTimeInstance().format(Date(it))
|
||||||
|
}}")
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
context.androidContext.externalCacheDir?.let { cacheDir ->
|
||||||
|
val cacheFile = coilCacheFile ?: run {
|
||||||
|
context.shortToast("Failed to get file")
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
|
val targetFile = File(cacheDir, cacheFile.name)
|
||||||
|
cacheFile.copyTo(targetFile, overwrite = true)
|
||||||
|
context.androidContext.startActivity(Intent().apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
setDataAndType(
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context.androidContext,
|
||||||
|
"me.rhunk.snapenhance.fileprovider",
|
||||||
|
targetFile
|
||||||
|
),
|
||||||
|
FileType.fromFile(targetFile).mimeType
|
||||||
|
)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(text = "Open")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
val mediaAuthor = friendInfo?.mutableUsername ?: userId
|
||||||
|
val uniqueHash = (selectedStory?.url ?: UUID.randomUUID().toString()).longHashCode().absoluteValue.toString(16)
|
||||||
|
|
||||||
|
DownloadProcessor(
|
||||||
|
remoteSideContext = context,
|
||||||
|
callback = object: DownloadCallback.Default() {
|
||||||
|
override fun onSuccess(outputPath: String?) {
|
||||||
|
context.shortToast("Downloaded to $outputPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(message: String?, throwable: String?) {
|
||||||
|
context.shortToast("Failed to download $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).enqueue(DownloadRequest(
|
||||||
|
inputMedias = arrayOf(
|
||||||
|
InputMedia(
|
||||||
|
content = story.url,
|
||||||
|
type = DownloadMediaType.REMOTE_MEDIA,
|
||||||
|
encryption = story.key?.let { it to story.iv!! }?.toKeyPair()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
), DownloadMetadata(
|
||||||
|
mediaIdentifier = uniqueHash,
|
||||||
|
outputPath = createNewFilePath(
|
||||||
|
context.config.root,
|
||||||
|
uniqueHash,
|
||||||
|
MediaDownloadSource.STORY_LOGGER,
|
||||||
|
mediaAuthor,
|
||||||
|
story.createdAt
|
||||||
|
),
|
||||||
|
iconUrl = null,
|
||||||
|
mediaAuthor = friendInfo?.mutableUsername ?: userId,
|
||||||
|
downloadSource = MediaDownloadSource.STORY_LOGGER.translate(context.translation),
|
||||||
|
))
|
||||||
|
}) {
|
||||||
|
Text(text = "Download")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stories.isEmpty()) {
|
||||||
|
Text(text = "No stories found", Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(100.dp),
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
) {
|
||||||
|
items(stories) { story ->
|
||||||
|
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
|
||||||
|
val uniqueHash = remember { story.url.hashCode().absoluteValue.toString(16) }
|
||||||
|
|
||||||
|
fun openDiskCacheSnapshot(snapshot: DiskCache.Snapshot): Boolean {
|
||||||
|
runCatching {
|
||||||
|
val mediaList = mutableMapOf<SplitMediaAssetType, ByteArray>()
|
||||||
|
|
||||||
|
snapshot.data.toFile().inputStream().use { inputStream ->
|
||||||
|
MediaDownloaderHelper.getSplitElements(inputStream) { type, splitInputStream ->
|
||||||
|
mediaList[type] = splitInputStream.readBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val originalMedia = mediaList[SplitMediaAssetType.ORIGINAL] ?: return@runCatching false
|
||||||
|
val overlay = mediaList[SplitMediaAssetType.OVERLAY]
|
||||||
|
|
||||||
|
var bitmap: Bitmap? = PreviewUtils.createPreview(originalMedia, isVideo = FileType.fromByteArray(originalMedia).isVideo)
|
||||||
|
|
||||||
|
overlay?.also {
|
||||||
|
bitmap = PreviewUtils.mergeBitmapOverlay(bitmap!!, BitmapFactory.decodeByteArray(it, 0, it.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
imageBitmap = bitmap?.asImageBitmap()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
withTimeout(10000L) {
|
||||||
|
context.imageLoader.diskCache?.openSnapshot(uniqueHash)?.let {
|
||||||
|
openDiskCacheSnapshot(it)
|
||||||
|
it.close()
|
||||||
|
return@withTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
val response = httpClient.newCall(Request(
|
||||||
|
url = story.url.toHttpUrl()
|
||||||
|
)).execute()
|
||||||
|
response.body.byteStream().use {
|
||||||
|
val decrypted = story.key?.let { _ ->
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(story.key, "AES"), IvParameterSpec(story.iv))
|
||||||
|
CipherInputStream(it, cipher)
|
||||||
|
} ?: it
|
||||||
|
|
||||||
|
context.imageLoader.diskCache?.openEditor(uniqueHash)?.apply {
|
||||||
|
data.toFile().outputStream().use { fos ->
|
||||||
|
decrypted.copyTo(fos)
|
||||||
|
}
|
||||||
|
commitAndOpenSnapshot()?.use { snapshot ->
|
||||||
|
openDiskCacheSnapshot(snapshot)
|
||||||
|
snapshot.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
context.log.error("Failed to load story", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.clickable {
|
||||||
|
selectedStory = story
|
||||||
|
coilCacheFile = context.imageLoader.diskCache?.openSnapshot(uniqueHash).use {
|
||||||
|
it?.data?.toFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.heightIn(min = 128.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
imageBitmap?.let {
|
||||||
|
Card {
|
||||||
|
Image(
|
||||||
|
bitmap = it,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
context.messageLogger.getStories(userId, lastStoryTimestamp, 20).also { result ->
|
||||||
|
stories.addAll(result.values)
|
||||||
|
result.keys.minOrNull()?.let {
|
||||||
|
lastStoryTimestamp = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,68 +1,90 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.social
|
package me.rhunk.snapenhance.ui.manager.pages.social
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material.icons.rounded.DeleteForever
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
|
||||||
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
import me.rhunk.snapenhance.common.data.MessagingRuleType
|
||||||
import me.rhunk.snapenhance.common.data.SocialScope
|
import me.rhunk.snapenhance.common.data.SocialScope
|
||||||
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
|
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
|
||||||
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
||||||
import me.rhunk.snapenhance.ui.util.BitmojiImage
|
import me.rhunk.snapenhance.ui.util.BitmojiImage
|
||||||
import me.rhunk.snapenhance.ui.util.Dialog
|
import me.rhunk.snapenhance.ui.util.Dialog
|
||||||
import kotlin.io.encoding.Base64
|
import kotlin.io.encoding.Base64
|
||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
class ScopeContent(
|
class ManageScope: Routes.Route() {
|
||||||
private val context: RemoteSideContext,
|
|
||||||
private val section: SocialSection,
|
|
||||||
private val navController: NavController,
|
|
||||||
val scope: SocialScope,
|
|
||||||
private val id: String
|
|
||||||
) {
|
|
||||||
private val dialogs by lazy { AlertDialogs(context.translation) }
|
private val dialogs by lazy { AlertDialogs(context.translation) }
|
||||||
private val translation by lazy { context.translation.getCategory("manager.sections.social") }
|
private val translation by lazy { context.translation.getCategory("manager.sections.social") }
|
||||||
|
|
||||||
fun deleteScope(coroutineScope: CoroutineScope) {
|
private fun deleteScope(scope: SocialScope, id: String, coroutineScope: CoroutineScope) {
|
||||||
when (scope) {
|
when (scope) {
|
||||||
SocialScope.FRIEND -> context.modDatabase.deleteFriend(id)
|
SocialScope.FRIEND -> context.modDatabase.deleteFriend(id)
|
||||||
SocialScope.GROUP -> context.modDatabase.deleteGroup(id)
|
SocialScope.GROUP -> context.modDatabase.deleteGroup(id)
|
||||||
}
|
}
|
||||||
context.modDatabase.executeAsync {
|
context.modDatabase.executeAsync {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
section.onResumed()
|
routes.navController.popBackStack()
|
||||||
navController.popBackStack()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val topBarActions: @Composable (RowScope.() -> Unit) = topBarActions@{
|
||||||
fun Content() {
|
val navBackStackEntry by routes.navController.currentBackStackEntryAsState()
|
||||||
|
var deleteConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
if (deleteConfirmDialog) {
|
||||||
|
val scope = navBackStackEntry?.arguments?.getString("scope")?.let { SocialScope.getByName(it) } ?: return@topBarActions
|
||||||
|
val id = navBackStackEntry?.arguments?.getString("id")!!
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = {
|
||||||
|
deleteConfirmDialog = false
|
||||||
|
}) {
|
||||||
|
remember { AlertDialogs(context.translation) }.ConfirmDialog(
|
||||||
|
title = "Are you sure you want to delete this ${scope.key.lowercase()}?",
|
||||||
|
onDismiss = { deleteConfirmDialog = false },
|
||||||
|
onConfirm = {
|
||||||
|
deleteScope(scope, id, coroutineScope); deleteConfirmDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { deleteConfirmDialog = true },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.DeleteForever,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val content: @Composable (NavBackStackEntry) -> Unit = content@{ navBackStackEntry ->
|
||||||
|
val scope = SocialScope.getByName(navBackStackEntry.arguments?.getString("scope")!!)
|
||||||
|
val id = navBackStackEntry.arguments?.getString("id")!!
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState())
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
when (scope) {
|
when (scope) {
|
||||||
SocialScope.FRIEND -> Friend()
|
SocialScope.FRIEND -> Friend(id)
|
||||||
SocialScope.GROUP -> Group()
|
SocialScope.GROUP -> Group(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@ -163,7 +185,7 @@ class ScopeContent(
|
|||||||
|
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun Friend() {
|
private fun Friend(id: String) {
|
||||||
//fetch the friend from the database
|
//fetch the friend from the database
|
||||||
val friend = remember { context.modDatabase.getFriendInfo(id) } ?: run {
|
val friend = remember { context.modDatabase.getFriendInfo(id) } ?: run {
|
||||||
Text(text = translation["not_found"])
|
Text(text = translation["not_found"])
|
||||||
@ -208,7 +230,9 @@ class ScopeContent(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally),
|
||||||
) {
|
) {
|
||||||
Button(onClick = {
|
Button(onClick = {
|
||||||
navController.navigate(SocialSection.LOGGED_STORIES_ROUTE.replace("{userId}", id))
|
routes.loggedStories.navigate {
|
||||||
|
put("id", id)
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("Show Logged Stories")
|
Text("Show Logged Stories")
|
||||||
}
|
}
|
||||||
@ -331,7 +355,7 @@ class ScopeContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Group() {
|
private fun Group(id: String) {
|
||||||
//fetch the group from the database
|
//fetch the group from the database
|
||||||
val group = remember { context.modDatabase.getGroupInfo(id) } ?: run {
|
val group = remember { context.modDatabase.getGroupInfo(id) } ?: run {
|
||||||
Text(text = translation["not_found"])
|
Text(text = translation["not_found"])
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.social
|
package me.rhunk.snapenhance.ui.manager.pages.social
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@ -23,8 +23,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
|
||||||
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
import me.rhunk.snapenhance.bridge.snapclient.MessagingBridge
|
||||||
import me.rhunk.snapenhance.bridge.snapclient.SessionStartListener
|
import me.rhunk.snapenhance.bridge.snapclient.SessionStartListener
|
||||||
import me.rhunk.snapenhance.bridge.snapclient.types.Message
|
import me.rhunk.snapenhance.bridge.snapclient.types.Message
|
||||||
@ -38,23 +38,21 @@ import me.rhunk.snapenhance.messaging.MessagingConstraints
|
|||||||
import me.rhunk.snapenhance.messaging.MessagingTask
|
import me.rhunk.snapenhance.messaging.MessagingTask
|
||||||
import me.rhunk.snapenhance.messaging.MessagingTaskConstraint
|
import me.rhunk.snapenhance.messaging.MessagingTaskConstraint
|
||||||
import me.rhunk.snapenhance.messaging.MessagingTaskType
|
import me.rhunk.snapenhance.messaging.MessagingTaskType
|
||||||
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.Dialog
|
import me.rhunk.snapenhance.ui.util.Dialog
|
||||||
|
import java.util.SortedMap
|
||||||
|
|
||||||
class MessagingPreview(
|
class MessagingPreview: Routes.Route() {
|
||||||
private val context: RemoteSideContext,
|
|
||||||
private val scope: SocialScope,
|
|
||||||
private val scopeId: String
|
|
||||||
) {
|
|
||||||
private lateinit var coroutineScope: CoroutineScope
|
private lateinit var coroutineScope: CoroutineScope
|
||||||
private lateinit var messagingBridge: MessagingBridge
|
private lateinit var messagingBridge: MessagingBridge
|
||||||
private lateinit var previewScrollState: LazyListState
|
private lateinit var previewScrollState: LazyListState
|
||||||
|
|
||||||
private val myUserId by lazy { messagingBridge.myUserId }
|
private val myUserId by lazy { messagingBridge.myUserId }
|
||||||
private val contentTypeTranslation by lazy { context.translation.getCategory("content_type") }
|
private val contentTypeTranslation by lazy { context.translation.getCategory("content_type") }
|
||||||
|
|
||||||
private var conversationId: String? = null
|
private var messages = sortedMapOf<Long, Message>()
|
||||||
private val messages = sortedMapOf<Long, Message>() // server message id => message
|
|
||||||
private var messageSize by mutableIntStateOf(0)
|
private var messageSize by mutableIntStateOf(0)
|
||||||
private var lastMessageId = Long.MAX_VALUE
|
private var conversationId by mutableStateOf<String?>(null)
|
||||||
private val selectedMessages = mutableStateListOf<Long>() // client message id
|
private val selectedMessages = mutableStateListOf<Long>() // client message id
|
||||||
|
|
||||||
private fun toggleSelectedMessage(messageId: Long) {
|
private fun toggleSelectedMessage(messageId: Long) {
|
||||||
@ -172,8 +170,7 @@ class MessagingPreview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val topBarActions: @Composable (RowScope.() -> Unit) = {
|
||||||
fun TopBarAction() {
|
|
||||||
var taskSelectionDropdown by remember { mutableStateOf(false) }
|
var taskSelectionDropdown by remember { mutableStateOf(false) }
|
||||||
var selectConstraintsDialog by remember { mutableStateOf(false) }
|
var selectConstraintsDialog by remember { mutableStateOf(false) }
|
||||||
var activeTask by remember { mutableStateOf(null as MessagingTask?) }
|
var activeTask by remember { mutableStateOf(null as MessagingTask?) }
|
||||||
@ -325,7 +322,11 @@ class MessagingPreview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConversationPreview() {
|
private fun ConversationPreview(
|
||||||
|
messages: SortedMap<Long, Message>,
|
||||||
|
messageSize: Int,
|
||||||
|
fetchNewMessages: () -> Unit
|
||||||
|
) {
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
selectedMessages.clear()
|
selectedMessages.clear()
|
||||||
@ -393,65 +394,6 @@ class MessagingPreview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchNewMessages() {
|
|
||||||
coroutineScope.launch(Dispatchers.IO) cs@{
|
|
||||||
runCatching {
|
|
||||||
val queriedMessages = messagingBridge.fetchConversationWithMessagesPaginated(
|
|
||||||
conversationId!!,
|
|
||||||
100,
|
|
||||||
lastMessageId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (queriedMessages == null) {
|
|
||||||
context.shortToast("Failed to fetch messages")
|
|
||||||
return@cs
|
|
||||||
}
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
messages.putAll(queriedMessages.map { it.serverMessageId to it })
|
|
||||||
messageSize = messages.size
|
|
||||||
if (queriedMessages.isNotEmpty()) {
|
|
||||||
lastMessageId = queriedMessages.first().clientMessageId
|
|
||||||
previewScrollState.scrollToItem(queriedMessages.size - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
context.shortToast("Failed to fetch messages: ${it.message}")
|
|
||||||
}
|
|
||||||
context.log.verbose("fetched ${messages.size} messages")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMessagingBridgeReady() {
|
|
||||||
runCatching {
|
|
||||||
messagingBridge = context.bridgeService!!.messagingBridge!!
|
|
||||||
conversationId = if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId
|
|
||||||
if (conversationId == null) {
|
|
||||||
context.longToast("Failed to fetch conversation id")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!messagingBridge.isSessionStarted) {
|
|
||||||
context.androidContext.packageManager.getLaunchIntentForPackage(
|
|
||||||
Constants.SNAPCHAT_PACKAGE_NAME
|
|
||||||
)?.let {
|
|
||||||
val mainIntent = Intent.makeRestartActivityTask(it.component).apply {
|
|
||||||
putExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA, true)
|
|
||||||
}
|
|
||||||
context.androidContext.startActivity(mainIntent)
|
|
||||||
}
|
|
||||||
messagingBridge.registerSessionStartListener(object: SessionStartListener.Stub() {
|
|
||||||
override fun onConnected() {
|
|
||||||
fetchNewMessages()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetchNewMessages()
|
|
||||||
}.onFailure {
|
|
||||||
context.longToast("Failed to initialize messaging bridge")
|
|
||||||
context.log.error("Failed to initialize messaging bridge", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LoadingRow() {
|
private fun LoadingRow() {
|
||||||
@ -471,41 +413,113 @@ class MessagingPreview(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
override val content: @Composable (NavBackStackEntry) -> Unit = { navBackStackEntry ->
|
||||||
fun Content() {
|
val scope = remember { SocialScope.getByName(navBackStackEntry.arguments?.getString("scope")!!) }
|
||||||
|
val id = remember { navBackStackEntry.arguments?.getString("id")!! }
|
||||||
|
|
||||||
previewScrollState = rememberLazyListState()
|
previewScrollState = rememberLazyListState()
|
||||||
coroutineScope = rememberCoroutineScope()
|
coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var lastMessageId by remember { mutableLongStateOf(Long.MAX_VALUE) }
|
||||||
var isBridgeConnected by remember { mutableStateOf(false) }
|
var isBridgeConnected by remember { mutableStateOf(false) }
|
||||||
var hasBridgeError by remember { mutableStateOf(false) }
|
var hasBridgeError by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun fetchNewMessages() {
|
||||||
|
coroutineScope.launch(Dispatchers.IO) cs@{
|
||||||
|
runCatching {
|
||||||
|
val queriedMessages = messagingBridge.fetchConversationWithMessagesPaginated(
|
||||||
|
conversationId!!,
|
||||||
|
20,
|
||||||
|
lastMessageId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (queriedMessages == null) {
|
||||||
|
context.shortToast("Failed to fetch messages")
|
||||||
|
return@cs
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
messages.putAll(queriedMessages.map { it.serverMessageId to it })
|
||||||
|
messageSize = messages.size
|
||||||
|
if (queriedMessages.isNotEmpty()) {
|
||||||
|
lastMessageId = queriedMessages.first().clientMessageId
|
||||||
|
delay(20)
|
||||||
|
previewScrollState.scrollToItem(queriedMessages.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
context.log.error("Failed to fetch messages", it)
|
||||||
|
context.shortToast("Failed to fetch messages: ${it.message}")
|
||||||
|
}
|
||||||
|
context.log.verbose("fetched ${messages.size} messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMessagingBridgeReady(scope: SocialScope, scopeId: String) {
|
||||||
|
context.log.verbose("onMessagingBridgeReady: $scope $scopeId")
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
messagingBridge = context.bridgeService!!.messagingBridge!!
|
||||||
|
conversationId = if (scope == SocialScope.FRIEND) messagingBridge.getOneToOneConversationId(scopeId) else scopeId
|
||||||
|
if (conversationId == null) {
|
||||||
|
context.longToast("Failed to fetch conversation id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (runCatching { !messagingBridge.isSessionStarted }.getOrDefault(true)) {
|
||||||
|
context.androidContext.packageManager.getLaunchIntentForPackage(
|
||||||
|
Constants.SNAPCHAT_PACKAGE_NAME
|
||||||
|
)?.let {
|
||||||
|
val mainIntent = Intent.makeRestartActivityTask(it.component).apply {
|
||||||
|
putExtra(ReceiversConfig.MESSAGING_PREVIEW_EXTRA, true)
|
||||||
|
}
|
||||||
|
context.androidContext.startActivity(mainIntent)
|
||||||
|
}
|
||||||
|
messagingBridge.registerSessionStartListener(object: SessionStartListener.Stub() {
|
||||||
|
override fun onConnected() {
|
||||||
|
fetchNewMessages()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchNewMessages()
|
||||||
|
}.onFailure {
|
||||||
|
context.longToast("Failed to initialize messaging bridge")
|
||||||
|
context.log.error("Failed to initialize messaging bridge", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
messages.clear()
|
||||||
|
messageSize = 0
|
||||||
|
conversationId = null
|
||||||
|
|
||||||
|
isBridgeConnected = context.hasMessagingBridge()
|
||||||
|
if (isBridgeConnected) {
|
||||||
|
onMessagingBridgeReady(scope, id)
|
||||||
|
} else {
|
||||||
|
SnapWidgetBroadcastReceiverHelper.create("wakeup") {}.also {
|
||||||
|
context.androidContext.sendBroadcast(it)
|
||||||
|
}
|
||||||
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
withTimeout(10000) {
|
||||||
|
while (!context.hasMessagingBridge()) {
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
isBridgeConnected = true
|
||||||
|
onMessagingBridgeReady(scope, id)
|
||||||
|
}
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
if (it != null) {
|
||||||
|
hasBridgeError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
isBridgeConnected = context.hasMessagingBridge()
|
|
||||||
if (isBridgeConnected) {
|
|
||||||
onMessagingBridgeReady()
|
|
||||||
} else {
|
|
||||||
SnapWidgetBroadcastReceiverHelper.create("wakeup") {}.also {
|
|
||||||
context.androidContext.sendBroadcast(it)
|
|
||||||
}
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
|
||||||
withTimeout(10000) {
|
|
||||||
while (!context.hasMessagingBridge()) {
|
|
||||||
delay(100)
|
|
||||||
}
|
|
||||||
isBridgeConnected = true
|
|
||||||
onMessagingBridgeReady()
|
|
||||||
}
|
|
||||||
}.invokeOnCompletion {
|
|
||||||
if (it != null) {
|
|
||||||
hasBridgeError = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasBridgeError) {
|
if (hasBridgeError) {
|
||||||
Text("Failed to connect to Snapchat through bridge service")
|
Text("Failed to connect to Snapchat through bridge service")
|
||||||
}
|
}
|
||||||
@ -515,7 +529,7 @@ class MessagingPreview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isBridgeConnected && !hasBridgeError) {
|
if (isBridgeConnected && !hasBridgeError) {
|
||||||
ConversationPreview()
|
ConversationPreview(messages, messageSize, ::fetchNewMessages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.social
|
package me.rhunk.snapenhance.ui.manager.pages.social
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@ -10,7 +10,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.RemoveRedEye
|
import androidx.compose.material.icons.filled.RemoveRedEye
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.DeleteForever
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -22,10 +21,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavGraphBuilder
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.navigation
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -34,119 +30,25 @@ import me.rhunk.snapenhance.common.data.MessagingFriendInfo
|
|||||||
import me.rhunk.snapenhance.common.data.MessagingGroupInfo
|
import me.rhunk.snapenhance.common.data.MessagingGroupInfo
|
||||||
import me.rhunk.snapenhance.common.data.SocialScope
|
import me.rhunk.snapenhance.common.data.SocialScope
|
||||||
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
|
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Routes
|
||||||
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
|
||||||
import me.rhunk.snapenhance.ui.util.BitmojiImage
|
import me.rhunk.snapenhance.ui.util.BitmojiImage
|
||||||
import me.rhunk.snapenhance.ui.util.pagerTabIndicatorOffset
|
import me.rhunk.snapenhance.ui.util.pagerTabIndicatorOffset
|
||||||
|
|
||||||
class SocialSection : Section() {
|
class SocialRoot : Routes.Route() {
|
||||||
private lateinit var friendList: List<MessagingFriendInfo>
|
private var friendList: List<MessagingFriendInfo> by mutableStateOf(emptyList())
|
||||||
private lateinit var groupList: List<MessagingGroupInfo>
|
private var groupList: List<MessagingGroupInfo> by mutableStateOf(emptyList())
|
||||||
|
|
||||||
companion object {
|
fun updateScopeLists() {
|
||||||
const val MAIN_ROUTE = "social_route"
|
context.coroutineScope.launch(Dispatchers.IO) {
|
||||||
const val MESSAGING_PREVIEW_ROUTE = "messaging_preview/?id={id}&scope={scope}"
|
friendList = context.modDatabase.getFriends(descOrder = true)
|
||||||
const val LOGGED_STORIES_ROUTE = "logged_stories/?userId={userId}"
|
groupList = context.modDatabase.getGroups()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentScopeContent: ScopeContent? = null
|
|
||||||
private var currentMessagingPreview by mutableStateOf(null as MessagingPreview?)
|
|
||||||
|
|
||||||
private val addFriendDialog by lazy {
|
private val addFriendDialog by lazy {
|
||||||
AddFriendDialog(context, this)
|
AddFriendDialog(context, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
//FIXME: don't reload the entire list when a friend is added/deleted
|
|
||||||
override fun onResumed() {
|
|
||||||
friendList = context.modDatabase.getFriends(descOrder = true)
|
|
||||||
groupList = context.modDatabase.getGroups()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canGoBack() = currentRoute != MAIN_ROUTE
|
|
||||||
|
|
||||||
override fun build(navGraphBuilder: NavGraphBuilder) {
|
|
||||||
navGraphBuilder.navigation(route = enumSection.route, startDestination = MAIN_ROUTE) {
|
|
||||||
composable(MAIN_ROUTE) {
|
|
||||||
Content()
|
|
||||||
}
|
|
||||||
|
|
||||||
SocialScope.entries.forEach { scope ->
|
|
||||||
composable(scope.tabRoute) {
|
|
||||||
val id = it.arguments?.getString("id") ?: return@composable
|
|
||||||
remember {
|
|
||||||
ScopeContent(
|
|
||||||
context,
|
|
||||||
this@SocialSection,
|
|
||||||
navController,
|
|
||||||
scope,
|
|
||||||
id
|
|
||||||
).also { tab ->
|
|
||||||
currentScopeContent = tab
|
|
||||||
}
|
|
||||||
}.Content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(LOGGED_STORIES_ROUTE) {
|
|
||||||
val userId = it.arguments?.getString("userId") ?: return@composable
|
|
||||||
LoggedStories(context, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(MESSAGING_PREVIEW_ROUTE) { navBackStackEntry ->
|
|
||||||
val id = navBackStackEntry.arguments?.getString("id") ?: return@composable
|
|
||||||
val scope = navBackStackEntry.arguments?.getString("scope") ?: return@composable
|
|
||||||
val messagePreview = remember {
|
|
||||||
MessagingPreview(context, SocialScope.getByName(scope), id)
|
|
||||||
}
|
|
||||||
LaunchedEffect(key1 = id) {
|
|
||||||
currentMessagingPreview = messagePreview
|
|
||||||
}
|
|
||||||
messagePreview.Content()
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose {
|
|
||||||
currentMessagingPreview = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun TopBarActions(rowScope: RowScope) {
|
|
||||||
var deleteConfirmDialog by remember { mutableStateOf(false) }
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
if (deleteConfirmDialog) {
|
|
||||||
currentScopeContent?.let { scopeContent ->
|
|
||||||
Dialog(onDismissRequest = { deleteConfirmDialog = false }) {
|
|
||||||
remember { AlertDialogs(context.translation) }.ConfirmDialog(
|
|
||||||
title = "Are you sure you want to delete this ${scopeContent.scope.key.lowercase()}?",
|
|
||||||
onDismiss = { deleteConfirmDialog = false },
|
|
||||||
onConfirm = {
|
|
||||||
scopeContent.deleteScope(coroutineScope); deleteConfirmDialog = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentRoute == MESSAGING_PREVIEW_ROUTE) {
|
|
||||||
currentMessagingPreview?.TopBarAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentRoute == SocialScope.FRIEND.tabRoute || currentRoute == SocialScope.GROUP.tabRoute) {
|
|
||||||
IconButton(
|
|
||||||
onClick = { deleteConfirmDialog = true },
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.DeleteForever,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScopeList(scope: SocialScope) {
|
private fun ScopeList(scope: SocialScope) {
|
||||||
val remainingHours = remember { context.config.root.streaksReminder.remainingHours.get() }
|
val remainingHours = remember { context.config.root.streaksReminder.remainingHours.get() }
|
||||||
@ -186,9 +88,10 @@ class SocialSection : Section() {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(80.dp)
|
.height(80.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
navController.navigate(
|
routes.manageScope.navigate {
|
||||||
scope.tabRoute.replace("{id}", id)
|
put("id", id)
|
||||||
)
|
put("scope", scope.key)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@ -275,9 +178,10 @@ class SocialSection : Section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FilledIconButton(onClick = {
|
FilledIconButton(onClick = {
|
||||||
navController.navigate(
|
routes.messagingPreview.navigate {
|
||||||
MESSAGING_PREVIEW_ROUTE.replace("{id}", id).replace("{scope}", scope.key)
|
put("id", id)
|
||||||
)
|
put("scope", scope.key)
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(imageVector = Icons.Filled.RemoveRedEye, contentDescription = null)
|
Icon(imageVector = Icons.Filled.RemoveRedEye, contentDescription = null)
|
||||||
}
|
}
|
||||||
@ -287,10 +191,8 @@ class SocialSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||||
override fun Content() {
|
|
||||||
val titles = listOf("Friends", "Groups")
|
val titles = listOf("Friends", "Groups")
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState { titles.size }
|
val pagerState = rememberPagerState { titles.size }
|
||||||
@ -302,6 +204,10 @@ class SocialSection : Section() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
updateScopeLists()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
@ -1,4 +0,0 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections
|
|
||||||
|
|
||||||
class DebugSection {
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
|
||||||
|
|
||||||
class NotImplemented : Section() {
|
|
||||||
@Composable
|
|
||||||
override fun Content() {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Text(text = "Not implemented yet!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.features
|
|
||||||
|
|
||||||
class PickLocation {
|
|
||||||
}
|
|
@ -1,276 +0,0 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.social
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import coil.annotation.ExperimentalCoilApi
|
|
||||||
import coil.disk.DiskCache
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
|
||||||
import me.rhunk.snapenhance.bridge.DownloadCallback
|
|
||||||
import me.rhunk.snapenhance.common.data.FileType
|
|
||||||
import me.rhunk.snapenhance.common.data.StoryData
|
|
||||||
import me.rhunk.snapenhance.common.data.download.*
|
|
||||||
import me.rhunk.snapenhance.common.util.ktx.longHashCode
|
|
||||||
import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper
|
|
||||||
import me.rhunk.snapenhance.core.util.media.PreviewUtils
|
|
||||||
import me.rhunk.snapenhance.download.DownloadProcessor
|
|
||||||
import me.rhunk.snapenhance.ui.util.Dialog
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import java.io.File
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.UUID
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.CipherInputStream
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
|
||||||
@Composable
|
|
||||||
fun LoggedStories(
|
|
||||||
context: RemoteSideContext,
|
|
||||||
userId: String
|
|
||||||
) {
|
|
||||||
val stories = remember {
|
|
||||||
mutableStateListOf<StoryData>()
|
|
||||||
}
|
|
||||||
val friendInfo = remember {
|
|
||||||
context.modDatabase.getFriendInfo(userId)
|
|
||||||
}
|
|
||||||
val httpClient = remember { OkHttpClient() }
|
|
||||||
var lastStoryTimestamp by remember { mutableLongStateOf(Long.MAX_VALUE) }
|
|
||||||
|
|
||||||
var selectedStory by remember { mutableStateOf<StoryData?>(null) }
|
|
||||||
var coilCacheFile by remember { mutableStateOf<File?>(null) }
|
|
||||||
|
|
||||||
selectedStory?.let { story ->
|
|
||||||
Dialog(onDismissRequest = {
|
|
||||||
selectedStory = null
|
|
||||||
}) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Text(text = "Posted on ${story.postedAt.let {
|
|
||||||
DateFormat.getDateTimeInstance().format(Date(it))
|
|
||||||
}}")
|
|
||||||
Text(text = "Created at ${story.createdAt.let {
|
|
||||||
DateFormat.getDateTimeInstance().format(Date(it))
|
|
||||||
}}")
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
) {
|
|
||||||
Button(onClick = {
|
|
||||||
context.androidContext.externalCacheDir?.let { cacheDir ->
|
|
||||||
val cacheFile = coilCacheFile ?: run {
|
|
||||||
context.shortToast("Failed to get file")
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
val targetFile = File(cacheDir, cacheFile.name)
|
|
||||||
cacheFile.copyTo(targetFile, overwrite = true)
|
|
||||||
context.androidContext.startActivity(Intent().apply {
|
|
||||||
action = Intent.ACTION_VIEW
|
|
||||||
setDataAndType(
|
|
||||||
FileProvider.getUriForFile(
|
|
||||||
context.androidContext,
|
|
||||||
"me.rhunk.snapenhance.fileprovider",
|
|
||||||
targetFile
|
|
||||||
),
|
|
||||||
FileType.fromFile(targetFile).mimeType
|
|
||||||
)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Text(text = "Open")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(onClick = {
|
|
||||||
val mediaAuthor = friendInfo?.mutableUsername ?: userId
|
|
||||||
val uniqueHash = (selectedStory?.url ?: UUID.randomUUID().toString()).longHashCode().absoluteValue.toString(16)
|
|
||||||
|
|
||||||
DownloadProcessor(
|
|
||||||
remoteSideContext = context,
|
|
||||||
callback = object: DownloadCallback.Default() {
|
|
||||||
override fun onSuccess(outputPath: String?) {
|
|
||||||
context.shortToast("Downloaded to $outputPath")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(message: String?, throwable: String?) {
|
|
||||||
context.shortToast("Failed to download $message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).enqueue(DownloadRequest(
|
|
||||||
inputMedias = arrayOf(
|
|
||||||
InputMedia(
|
|
||||||
content = story.url,
|
|
||||||
type = DownloadMediaType.REMOTE_MEDIA,
|
|
||||||
encryption = story.key?.let { it to story.iv!! }?.toKeyPair()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
), DownloadMetadata(
|
|
||||||
mediaIdentifier = uniqueHash,
|
|
||||||
outputPath = createNewFilePath(
|
|
||||||
context.config.root,
|
|
||||||
uniqueHash,
|
|
||||||
MediaDownloadSource.STORY_LOGGER,
|
|
||||||
mediaAuthor,
|
|
||||||
story.createdAt
|
|
||||||
),
|
|
||||||
iconUrl = null,
|
|
||||||
mediaAuthor = friendInfo?.mutableUsername ?: userId,
|
|
||||||
downloadSource = MediaDownloadSource.STORY_LOGGER.translate(context.translation),
|
|
||||||
))
|
|
||||||
}) {
|
|
||||||
Text(text = "Download")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stories.isEmpty()) {
|
|
||||||
Text(text = "No stories found", Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Adaptive(100.dp),
|
|
||||||
contentPadding = PaddingValues(8.dp),
|
|
||||||
) {
|
|
||||||
items(stories) { story ->
|
|
||||||
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
|
|
||||||
val uniqueHash = remember { story.url.hashCode().absoluteValue.toString(16) }
|
|
||||||
|
|
||||||
fun openDiskCacheSnapshot(snapshot: DiskCache.Snapshot): Boolean {
|
|
||||||
runCatching {
|
|
||||||
val mediaList = mutableMapOf<SplitMediaAssetType, ByteArray>()
|
|
||||||
|
|
||||||
snapshot.data.toFile().inputStream().use { inputStream ->
|
|
||||||
MediaDownloaderHelper.getSplitElements(inputStream) { type, splitInputStream ->
|
|
||||||
mediaList[type] = splitInputStream.readBytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val originalMedia = mediaList[SplitMediaAssetType.ORIGINAL] ?: return@runCatching false
|
|
||||||
val overlay = mediaList[SplitMediaAssetType.OVERLAY]
|
|
||||||
|
|
||||||
var bitmap: Bitmap? = PreviewUtils.createPreview(originalMedia, isVideo = FileType.fromByteArray(originalMedia).isVideo)
|
|
||||||
|
|
||||||
overlay?.also {
|
|
||||||
bitmap = PreviewUtils.mergeBitmapOverlay(bitmap!!, BitmapFactory.decodeByteArray(it, 0, it.size))
|
|
||||||
}
|
|
||||||
|
|
||||||
imageBitmap = bitmap?.asImageBitmap()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
withTimeout(10000L) {
|
|
||||||
context.imageLoader.diskCache?.openSnapshot(uniqueHash)?.let {
|
|
||||||
openDiskCacheSnapshot(it)
|
|
||||||
it.close()
|
|
||||||
return@withTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
val response = httpClient.newCall(Request(
|
|
||||||
url = story.url.toHttpUrl()
|
|
||||||
)).execute()
|
|
||||||
response.body.byteStream().use {
|
|
||||||
val decrypted = story.key?.let { _ ->
|
|
||||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(story.key, "AES"), IvParameterSpec(story.iv))
|
|
||||||
CipherInputStream(it, cipher)
|
|
||||||
} ?: it
|
|
||||||
|
|
||||||
context.imageLoader.diskCache?.openEditor(uniqueHash)?.apply {
|
|
||||||
data.toFile().outputStream().use { fos ->
|
|
||||||
decrypted.copyTo(fos)
|
|
||||||
}
|
|
||||||
commitAndOpenSnapshot()?.use { snapshot ->
|
|
||||||
openDiskCacheSnapshot(snapshot)
|
|
||||||
snapshot.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
context.log.error("Failed to load story", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp)
|
|
||||||
.clickable {
|
|
||||||
selectedStory = story
|
|
||||||
coilCacheFile = context.imageLoader.diskCache?.openSnapshot(uniqueHash).use {
|
|
||||||
it?.data?.toFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.heightIn(min = 128.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
imageBitmap?.let {
|
|
||||||
Card {
|
|
||||||
Image(
|
|
||||||
bitmap = it,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
context.messageLogger.getStories(userId, lastStoryTimestamp, 20).also { result ->
|
|
||||||
stories.addAll(result.values)
|
|
||||||
result.keys.minOrNull()?.let {
|
|
||||||
lastStoryTimestamp = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -25,9 +25,7 @@ import com.arthenica.ffmpegkit.Packages.getPackageName
|
|||||||
import me.rhunk.snapenhance.R
|
import me.rhunk.snapenhance.R
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.common.ui.createComposeView
|
import me.rhunk.snapenhance.common.ui.createComposeView
|
||||||
import me.rhunk.snapenhance.ui.manager.EnumSection
|
|
||||||
import me.rhunk.snapenhance.ui.manager.Navigation
|
import me.rhunk.snapenhance.ui.manager.Navigation
|
||||||
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsOverlay(
|
class SettingsOverlay(
|
||||||
@ -55,26 +53,15 @@ class SettingsOverlay(
|
|||||||
dismissCallback = { navHostController.popBackStack() }
|
dismissCallback = { navHostController.popBackStack() }
|
||||||
}
|
}
|
||||||
|
|
||||||
val navigation = remember {
|
val navigation = remember { Navigation(context, navHostController) }
|
||||||
Navigation(
|
|
||||||
context,
|
|
||||||
mapOf(
|
|
||||||
EnumSection.FEATURES to FeaturesSection().apply {
|
|
||||||
enumSection = EnumSection.FEATURES
|
|
||||||
context = this@SettingsOverlay.context
|
|
||||||
}
|
|
||||||
),
|
|
||||||
navHostController
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
topBar = { navigation.TopBar() }
|
topBar = { navigation.TopBar() }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
navigation.NavigationHost(
|
navigation.Content(
|
||||||
startDestination = EnumSection.FEATURES,
|
innerPadding,
|
||||||
innerPadding = innerPadding
|
startDestination = navigation.routes.features.routeInfo.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
"home_settings": "Settings",
|
"home_settings": "Settings",
|
||||||
"home_logs": "Logs",
|
"home_logs": "Logs",
|
||||||
"social": "Social",
|
"social": "Social",
|
||||||
|
"manage_scope": "Manage Scope",
|
||||||
|
"messaging_preview": "Preview",
|
||||||
"scripts": "Scripts"
|
"scripts": "Scripts"
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
|
@ -110,7 +110,6 @@ class BridgeClient(
|
|||||||
return runCatching {
|
return runCatching {
|
||||||
block()
|
block()
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
context.log.error("failed to call service", it)
|
|
||||||
if (it is DeadObjectException) {
|
if (it is DeadObjectException) {
|
||||||
context.softRestartApp()
|
context.softRestartApp()
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user