mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
feat: features overlay
- rename debug section to settings
This commit is contained in:
@ -5,6 +5,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<package android:name="com.snapchat.android" />
|
<package android:name="com.snapchat.android" />
|
||||||
|
@ -25,6 +25,7 @@ import me.rhunk.snapenhance.download.DownloadTaskManager
|
|||||||
import me.rhunk.snapenhance.messaging.ModDatabase
|
import me.rhunk.snapenhance.messaging.ModDatabase
|
||||||
import me.rhunk.snapenhance.messaging.StreaksReminder
|
import me.rhunk.snapenhance.messaging.StreaksReminder
|
||||||
import me.rhunk.snapenhance.scripting.RemoteScriptManager
|
import me.rhunk.snapenhance.scripting.RemoteScriptManager
|
||||||
|
import me.rhunk.snapenhance.ui.overlay.SettingsOverlay
|
||||||
import me.rhunk.snapenhance.ui.manager.MainActivity
|
import me.rhunk.snapenhance.ui.manager.MainActivity
|
||||||
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
|
||||||
import me.rhunk.snapenhance.ui.manager.data.ModInfo
|
import me.rhunk.snapenhance.ui.manager.data.ModInfo
|
||||||
@ -56,6 +57,7 @@ class RemoteSideContext(
|
|||||||
val streaksReminder = StreaksReminder(this)
|
val streaksReminder = StreaksReminder(this)
|
||||||
val log = LogManager(this)
|
val log = LogManager(this)
|
||||||
val scriptManager = RemoteScriptManager(this)
|
val scriptManager = RemoteScriptManager(this)
|
||||||
|
val settingsOverlay = SettingsOverlay(this)
|
||||||
|
|
||||||
//used to load bitmoji selfies and download previews
|
//used to load bitmoji selfies and download previews
|
||||||
val imageLoader by lazy {
|
val imageLoader by lazy {
|
||||||
|
@ -187,5 +187,20 @@ class BridgeService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getScriptingInterface() = remoteSideContext.scriptManager
|
override fun getScriptingInterface() = remoteSideContext.scriptManager
|
||||||
|
override fun openSettingsOverlay() {
|
||||||
|
runCatching {
|
||||||
|
remoteSideContext.settingsOverlay.show()
|
||||||
|
}.onFailure {
|
||||||
|
remoteSideContext.log.error("Failed to open settings overlay", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun closeSettingsOverlay() {
|
||||||
|
runCatching {
|
||||||
|
remoteSideContext.settingsOverlay.close()
|
||||||
|
}.onFailure {
|
||||||
|
remoteSideContext.log.error("Failed to close settings overlay", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package me.rhunk.snapenhance.ui.manager.sections.features
|
package me.rhunk.snapenhance.ui.manager.sections.features
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@ -65,7 +66,6 @@ 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.compose.ui.window.Dialog
|
|
||||||
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
|
||||||
@ -80,12 +80,9 @@ import me.rhunk.snapenhance.core.config.FeatureNotice
|
|||||||
import me.rhunk.snapenhance.core.config.PropertyKey
|
import me.rhunk.snapenhance.core.config.PropertyKey
|
||||||
import me.rhunk.snapenhance.core.config.PropertyPair
|
import me.rhunk.snapenhance.core.config.PropertyPair
|
||||||
import me.rhunk.snapenhance.core.config.PropertyValue
|
import me.rhunk.snapenhance.core.config.PropertyValue
|
||||||
|
import me.rhunk.snapenhance.ui.manager.MainActivity
|
||||||
import me.rhunk.snapenhance.ui.manager.Section
|
import me.rhunk.snapenhance.ui.manager.Section
|
||||||
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
|
import me.rhunk.snapenhance.ui.util.*
|
||||||
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
|
||||||
import me.rhunk.snapenhance.ui.util.chooseFolder
|
|
||||||
import me.rhunk.snapenhance.ui.util.openFile
|
|
||||||
import me.rhunk.snapenhance.ui.util.saveFile
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class FeaturesSection : Section() {
|
class FeaturesSection : Section() {
|
||||||
@ -98,7 +95,7 @@ class FeaturesSection : Section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
private var activityLauncherHelper: ActivityLauncherHelper? = null
|
||||||
private val featuresRouteName by lazy { context.translation["manager.routes.features"] }
|
private val featuresRouteName by lazy { context.translation["manager.routes.features"] }
|
||||||
|
|
||||||
private lateinit var rememberScaffoldState: BottomSheetScaffoldState
|
private lateinit var rememberScaffoldState: BottomSheetScaffoldState
|
||||||
@ -143,6 +140,16 @@ class FeaturesSection : Section() {
|
|||||||
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun activityLauncher(block: ActivityLauncherHelper.() -> Unit) {
|
||||||
|
activityLauncherHelper?.let(block) ?: run {
|
||||||
|
//open manager if activity launcher is null
|
||||||
|
val intent = Intent(context.androidContext, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
intent.putExtra("route", enumSection.route)
|
||||||
|
context.androidContext.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(navGraphBuilder: NavGraphBuilder) {
|
override fun build(navGraphBuilder: NavGraphBuilder) {
|
||||||
navGraphBuilder.navigation(route = enumSection.route, startDestination = MAIN_ROUTE) {
|
navGraphBuilder.navigation(route = enumSection.route, startDestination = MAIN_ROUTE) {
|
||||||
composable(MAIN_ROUTE) {
|
composable(MAIN_ROUTE) {
|
||||||
@ -194,8 +201,10 @@ class FeaturesSection : Section() {
|
|||||||
|
|
||||||
if (property.key.params.flags.contains(ConfigFlag.FOLDER)) {
|
if (property.key.params.flags.contains(ConfigFlag.FOLDER)) {
|
||||||
IconButton(onClick = registerClickCallback {
|
IconButton(onClick = registerClickCallback {
|
||||||
activityLauncherHelper.chooseFolder { uri ->
|
activityLauncher {
|
||||||
propertyValue.setAny(uri)
|
chooseFolder { uri ->
|
||||||
|
propertyValue.setAny(uri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.let { { it.invoke(true) } }) {
|
}.let { { it.invoke(true) } }) {
|
||||||
Icon(Icons.Filled.FolderOpen, contentDescription = null)
|
Icon(Icons.Filled.FolderOpen, contentDescription = null)
|
||||||
@ -478,24 +487,28 @@ class FeaturesSection : Section() {
|
|||||||
val actions = remember {
|
val actions = remember {
|
||||||
mapOf(
|
mapOf(
|
||||||
"Export" to {
|
"Export" to {
|
||||||
activityLauncherHelper.saveFile("config.json", "application/json") { uri ->
|
activityLauncher {
|
||||||
context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
|
saveFile("config.json", "application/json") { uri ->
|
||||||
context.config.writeConfig()
|
context.androidContext.contentResolver.openOutputStream(Uri.parse(uri))?.use {
|
||||||
context.config.exportToString().byteInputStream().copyTo(it)
|
context.config.writeConfig()
|
||||||
context.shortToast("Config exported successfully!")
|
context.config.exportToString().byteInputStream().copyTo(it)
|
||||||
|
context.shortToast("Config exported successfully!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Import" to {
|
"Import" to {
|
||||||
activityLauncherHelper.openFile("application/json") { uri ->
|
activityLauncher {
|
||||||
context.androidContext.contentResolver.openInputStream(Uri.parse(uri))?.use {
|
openFile("application/json") { uri ->
|
||||||
runCatching {
|
context.androidContext.contentResolver.openInputStream(Uri.parse(uri))?.use {
|
||||||
context.config.loadFromString(it.readBytes().toString(Charsets.UTF_8))
|
runCatching {
|
||||||
}.onFailure {
|
context.config.loadFromString(it.readBytes().toString(Charsets.UTF_8))
|
||||||
context.longToast("Failed to import config ${it.message}")
|
}.onFailure {
|
||||||
return@use
|
context.longToast("Failed to import config ${it.message}")
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
context.shortToast("Config successfully loaded!")
|
||||||
}
|
}
|
||||||
context.shortToast("Config successfully loaded!")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4,32 +4,11 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BugReport
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material.icons.filled.Map
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
|
||||||
import androidx.compose.material.icons.filled.OpenInNew
|
|
||||||
import androidx.compose.material.icons.filled.ReceiptLong
|
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -63,8 +42,8 @@ class HomeSection : Section() {
|
|||||||
companion object {
|
companion object {
|
||||||
val cardMargin = 10.dp
|
val cardMargin = 10.dp
|
||||||
const val HOME_ROOT = "home_root"
|
const val HOME_ROOT = "home_root"
|
||||||
const val DEBUG_SECTION_ROUTE = "home_debug"
|
|
||||||
const val LOGS_SECTION_ROUTE = "home_logs"
|
const val LOGS_SECTION_ROUTE = "home_logs"
|
||||||
|
const val SETTINGS_SECTION_ROUTE = "home_settings"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var installationSummary: InstallationSummary? = null
|
private var installationSummary: InstallationSummary? = null
|
||||||
@ -223,12 +202,12 @@ class HomeSection : Section() {
|
|||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
navController.navigate(LOGS_SECTION_ROUTE)
|
navController.navigate(LOGS_SECTION_ROUTE)
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Filled.ReceiptLong, contentDescription = null)
|
Icon(Icons.Filled.BugReport, contentDescription = null)
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
navController.navigate(DEBUG_SECTION_ROUTE)
|
navController.navigate(SETTINGS_SECTION_ROUTE)
|
||||||
}) {
|
}) {
|
||||||
Icon(Icons.Filled.BugReport, contentDescription = null)
|
Icon(Icons.Filled.Settings, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOGS_SECTION_ROUTE -> {
|
LOGS_SECTION_ROUTE -> {
|
||||||
@ -290,8 +269,8 @@ class HomeSection : Section() {
|
|||||||
composable(LOGS_SECTION_ROUTE) {
|
composable(LOGS_SECTION_ROUTE) {
|
||||||
homeSubSection.LogsSection()
|
homeSubSection.LogsSection()
|
||||||
}
|
}
|
||||||
composable(DEBUG_SECTION_ROUTE) {
|
composable(SETTINGS_SECTION_ROUTE) {
|
||||||
homeSubSection.DebugSection()
|
SettingsSection().also { it.context = context }.Content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,26 +2,14 @@ package me.rhunk.snapenhance.ui.manager.sections.home
|
|||||||
|
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown
|
import androidx.compose.material.icons.filled.KeyboardDoubleArrowDown
|
||||||
import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp
|
import androidx.compose.material.icons.filled.KeyboardDoubleArrowUp
|
||||||
import androidx.compose.material.icons.filled.OpenInNew
|
|
||||||
import androidx.compose.material.icons.outlined.BugReport
|
import androidx.compose.material.icons.outlined.BugReport
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.Report
|
import androidx.compose.material.icons.outlined.Report
|
||||||
@ -29,17 +17,9 @@ import androidx.compose.material.icons.outlined.Warning
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.FilledIconButton
|
import androidx.compose.material3.FilledIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
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.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@ -49,72 +29,18 @@ 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.compose.ui.window.Dialog
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.rhunk.snapenhance.Constants
|
|
||||||
import me.rhunk.snapenhance.core.logger.LogChannel
|
|
||||||
import me.rhunk.snapenhance.core.logger.LogLevel
|
|
||||||
import me.rhunk.snapenhance.LogReader
|
import me.rhunk.snapenhance.LogReader
|
||||||
import me.rhunk.snapenhance.RemoteSideContext
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
import me.rhunk.snapenhance.action.EnumAction
|
import me.rhunk.snapenhance.core.logger.LogChannel
|
||||||
import me.rhunk.snapenhance.core.bridge.types.BridgeFileType
|
import me.rhunk.snapenhance.core.logger.LogLevel
|
||||||
import me.rhunk.snapenhance.manager.impl.ActionManager
|
|
||||||
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
|
||||||
|
|
||||||
class HomeSubSection(
|
class HomeSubSection(
|
||||||
private val context: RemoteSideContext
|
private val context: RemoteSideContext
|
||||||
) {
|
) {
|
||||||
private val dialogs by lazy { AlertDialogs(context.translation) }
|
|
||||||
|
|
||||||
private lateinit var logListState: LazyListState
|
private lateinit var logListState: LazyListState
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RowAction(title: String, requireConfirmation: Boolean = false, action: () -> Unit) {
|
|
||||||
var confirmationDialog by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun takeAction() {
|
|
||||||
if (requireConfirmation) {
|
|
||||||
confirmationDialog = true
|
|
||||||
} else {
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requireConfirmation && confirmationDialog) {
|
|
||||||
Dialog(onDismissRequest = { confirmationDialog = false }) {
|
|
||||||
dialogs.ConfirmDialog(title = "Are you sure?", onConfirm = {
|
|
||||||
action()
|
|
||||||
confirmationDialog = false
|
|
||||||
}, onDismiss = {
|
|
||||||
confirmationDialog = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(65.dp)
|
|
||||||
.clickable {
|
|
||||||
takeAction()
|
|
||||||
},
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(text = title, modifier = Modifier.padding(start = 26.dp))
|
|
||||||
IconButton(onClick = { takeAction() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.OpenInNew,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogsSection() {
|
fun LogsSection() {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
@ -236,43 +162,4 @@ class HomeSubSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchActionIntent(action: EnumAction) {
|
|
||||||
val intent = context.androidContext.packageManager.getLaunchIntentForPackage(Constants.SNAPCHAT_PACKAGE_NAME)
|
|
||||||
intent?.putExtra(ActionManager.ACTION_PARAMETER, action.key)
|
|
||||||
context.androidContext.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RowTitle(title: String) {
|
|
||||||
Text(text = title, modifier = Modifier.padding(16.dp), fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DebugSection() {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(ScrollState(0))
|
|
||||||
) {
|
|
||||||
RowTitle(title = "Actions")
|
|
||||||
EnumAction.values().forEach { enumAction ->
|
|
||||||
RowAction(title = context.translation["actions.${enumAction.key}"]) {
|
|
||||||
launchActionIntent(enumAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowTitle(title = "Clear Files")
|
|
||||||
BridgeFileType.values().forEach { fileType ->
|
|
||||||
RowAction(title = fileType.displayName, requireConfirmation = true) {
|
|
||||||
runCatching {
|
|
||||||
fileType.resolve(context.androidContext).delete()
|
|
||||||
context.longToast("Deleted ${fileType.displayName}!")
|
|
||||||
}.onFailure {
|
|
||||||
context.longToast("Failed to delete ${fileType.displayName}!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.manager.sections.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.OpenInNew
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import me.rhunk.snapenhance.Constants
|
||||||
|
import me.rhunk.snapenhance.action.EnumAction
|
||||||
|
import me.rhunk.snapenhance.core.bridge.types.BridgeFileType
|
||||||
|
import me.rhunk.snapenhance.manager.impl.ActionManager
|
||||||
|
import me.rhunk.snapenhance.ui.manager.Section
|
||||||
|
import me.rhunk.snapenhance.ui.util.AlertDialogs
|
||||||
|
|
||||||
|
class SettingsSection : Section() {
|
||||||
|
private val dialogs by lazy { AlertDialogs(context.translation) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowTitle(title: String) {
|
||||||
|
Text(text = title, modifier = Modifier.padding(16.dp), fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowAction(title: String, requireConfirmation: Boolean = false, action: () -> Unit) {
|
||||||
|
var confirmationDialog by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun takeAction() {
|
||||||
|
if (requireConfirmation) {
|
||||||
|
confirmationDialog = true
|
||||||
|
} else {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireConfirmation && confirmationDialog) {
|
||||||
|
Dialog(onDismissRequest = { confirmationDialog = false }) {
|
||||||
|
dialogs.ConfirmDialog(title = "Are you sure?", onConfirm = {
|
||||||
|
action()
|
||||||
|
confirmationDialog = false
|
||||||
|
}, onDismiss = {
|
||||||
|
confirmationDialog = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(65.dp)
|
||||||
|
.clickable {
|
||||||
|
takeAction()
|
||||||
|
},
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = title, modifier = Modifier.padding(start = 26.dp))
|
||||||
|
IconButton(onClick = { takeAction() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.OpenInNew,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchActionIntent(action: EnumAction) {
|
||||||
|
val intent = context.androidContext.packageManager.getLaunchIntentForPackage(Constants.SNAPCHAT_PACKAGE_NAME)
|
||||||
|
intent?.putExtra(ActionManager.ACTION_PARAMETER, action.key)
|
||||||
|
context.androidContext.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(ScrollState(0))
|
||||||
|
) {
|
||||||
|
RowTitle(title = "Actions")
|
||||||
|
EnumAction.values().forEach { enumAction ->
|
||||||
|
RowAction(title = context.translation["actions.${enumAction.key}"]) {
|
||||||
|
launchActionIntent(enumAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowTitle(title = "Clear Files")
|
||||||
|
BridgeFileType.values().forEach { fileType ->
|
||||||
|
RowAction(title = fileType.displayName, requireConfirmation = true) {
|
||||||
|
runCatching {
|
||||||
|
fileType.resolve(context.androidContext).delete()
|
||||||
|
context.longToast("Deleted ${fileType.displayName}!")
|
||||||
|
}.onFailure {
|
||||||
|
context.longToast("Failed to delete ${fileType.displayName}!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.overlay
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.OnBackPressedDispatcher
|
||||||
|
import androidx.activity.OnBackPressedDispatcherOwner
|
||||||
|
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
|
||||||
|
import androidx.compose.runtime.Recomposer
|
||||||
|
import androidx.compose.ui.platform.AndroidUiDispatcher
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.compose.ui.platform.compositionContext
|
||||||
|
import androidx.lifecycle.*
|
||||||
|
import androidx.savedstate.SavedStateRegistry
|
||||||
|
import androidx.savedstate.SavedStateRegistryController
|
||||||
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
// https://github.com/tberghuis/FloatingCountdownTimer/blob/master/app/src/main/java/xyz/tberghuis/floatingtimer/service/overlayViewFactory.kt
|
||||||
|
fun overlayComposeView(service: Context) = ComposeView(service).apply {
|
||||||
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
|
val lifecycleOwner = OverlayLifecycleOwner().apply {
|
||||||
|
performRestore(null)
|
||||||
|
handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||||
|
}
|
||||||
|
setViewTreeLifecycleOwner(lifecycleOwner)
|
||||||
|
setViewTreeSavedStateRegistryOwner(lifecycleOwner)
|
||||||
|
|
||||||
|
val viewModelStore = ViewModelStore()
|
||||||
|
setViewTreeViewModelStoreOwner(object : ViewModelStoreOwner {
|
||||||
|
override val viewModelStore: ViewModelStore
|
||||||
|
get() = viewModelStore
|
||||||
|
})
|
||||||
|
|
||||||
|
val backPressedDispatcherOwner = OnBackPressedDispatcher()
|
||||||
|
setViewTreeOnBackPressedDispatcherOwner(object: OnBackPressedDispatcherOwner {
|
||||||
|
override val lifecycle: Lifecycle
|
||||||
|
get() = lifecycleOwner.lifecycle
|
||||||
|
override val onBackPressedDispatcher: OnBackPressedDispatcher
|
||||||
|
get() = backPressedDispatcherOwner
|
||||||
|
})
|
||||||
|
|
||||||
|
val coroutineContext = AndroidUiDispatcher.CurrentThread
|
||||||
|
val runRecomposeScope = CoroutineScope(coroutineContext)
|
||||||
|
val recomposer = Recomposer(coroutineContext)
|
||||||
|
compositionContext = recomposer
|
||||||
|
runRecomposeScope.launch {
|
||||||
|
recomposer.runRecomposeAndApplyChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OverlayLifecycleOwner : SavedStateRegistryOwner {
|
||||||
|
private var mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||||
|
private var mSavedStateRegistryController: SavedStateRegistryController =
|
||||||
|
SavedStateRegistryController.create(this)
|
||||||
|
override val lifecycle: Lifecycle
|
||||||
|
get() = mLifecycleRegistry
|
||||||
|
override val savedStateRegistry: SavedStateRegistry
|
||||||
|
get() = mSavedStateRegistryController.savedStateRegistry
|
||||||
|
fun handleLifecycleEvent(event: Lifecycle.Event) {
|
||||||
|
mLifecycleRegistry.handleLifecycleEvent(event)
|
||||||
|
}
|
||||||
|
fun performRestore(savedState: Bundle?) {
|
||||||
|
mSavedStateRegistryController.performRestore(savedState)
|
||||||
|
}
|
||||||
|
fun performSave(outBundle: Bundle) {
|
||||||
|
mSavedStateRegistryController.performSave(outBundle)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.overlay
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.arthenica.ffmpegkit.Packages.getPackageName
|
||||||
|
import me.rhunk.snapenhance.R
|
||||||
|
import me.rhunk.snapenhance.RemoteSideContext
|
||||||
|
import me.rhunk.snapenhance.ui.AppMaterialTheme
|
||||||
|
import me.rhunk.snapenhance.ui.manager.EnumSection
|
||||||
|
import me.rhunk.snapenhance.ui.manager.Navigation
|
||||||
|
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsOverlay(
|
||||||
|
private val context: RemoteSideContext
|
||||||
|
) {
|
||||||
|
private lateinit var dialog: Dialog
|
||||||
|
private fun checkForPermissions(): Boolean {
|
||||||
|
if (!Settings.canDrawOverlays(context.androidContext)) {
|
||||||
|
val myIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
|
||||||
|
myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
myIntent.setData(Uri.parse("package:" + getPackageName()))
|
||||||
|
context.androidContext.startActivity(myIntent)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OverlayContent() {
|
||||||
|
val navHostController = rememberNavController()
|
||||||
|
|
||||||
|
/*navHostController.addOnDestinationChangedListener { _, destination, _ ->
|
||||||
|
dialog.setCancelable(destination.route == FeaturesSection.MAIN_ROUTE)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val navigation = remember {
|
||||||
|
Navigation(
|
||||||
|
context,
|
||||||
|
mapOf(
|
||||||
|
EnumSection.FEATURES to FeaturesSection().apply {
|
||||||
|
enumSection = EnumSection.FEATURES
|
||||||
|
context = this@SettingsOverlay.context
|
||||||
|
}
|
||||||
|
),
|
||||||
|
navHostController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
|
topBar = { navigation.TopBar() }
|
||||||
|
) { innerPadding ->
|
||||||
|
navigation.NavigationHost(
|
||||||
|
startDestination = EnumSection.FEATURES,
|
||||||
|
innerPadding = innerPadding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
if (!::dialog.isInitialized || !dialog.isShowing) return
|
||||||
|
context.config.writeConfig()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
if (!checkForPermissions()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (::dialog.isInitialized && dialog.isShowing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
context.androidContext.mainExecutor.execute {
|
||||||
|
dialog = Dialog(context.androidContext, R.style.FullscreenOverlayDialog)
|
||||||
|
dialog.window?.apply {
|
||||||
|
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||||
|
setLayout(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
)
|
||||||
|
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
|
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.setContentView(
|
||||||
|
overlayComposeView(context.androidContext).apply {
|
||||||
|
setContent {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(10.dp)
|
||||||
|
.clip(shape = MaterialTheme.shapes.large),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
AppMaterialTheme {
|
||||||
|
OverlayContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.setCancelable(true)
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,360 @@
|
|||||||
|
package me.rhunk.snapenhance.ui.util
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.*
|
||||||
|
import androidx.activity.ComponentDialog
|
||||||
|
import androidx.activity.addCallback
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.R
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.platform.AbstractComposeView
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.platform.ViewRootForInspector
|
||||||
|
import androidx.compose.ui.semantics.dialog
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.SecureFlagPolicy
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
|
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
||||||
|
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||||
|
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||||
|
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
|
||||||
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class DialogProperties constructor(
|
||||||
|
val dismissOnBackPress: Boolean = true,
|
||||||
|
val dismissOnClickOutside: Boolean = true,
|
||||||
|
val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
|
||||||
|
val usePlatformDefaultWidth: Boolean = true,
|
||||||
|
val decorFitsSystemWindows: Boolean = true
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
dismissOnBackPress: Boolean = true,
|
||||||
|
dismissOnClickOutside: Boolean = true,
|
||||||
|
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
|
||||||
|
) : this(
|
||||||
|
dismissOnBackPress = dismissOnBackPress,
|
||||||
|
dismissOnClickOutside = dismissOnClickOutside,
|
||||||
|
securePolicy = securePolicy,
|
||||||
|
usePlatformDefaultWidth = true,
|
||||||
|
decorFitsSystemWindows = true
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is DialogProperties) return false
|
||||||
|
|
||||||
|
if (dismissOnBackPress != other.dismissOnBackPress) return false
|
||||||
|
if (dismissOnClickOutside != other.dismissOnClickOutside) return false
|
||||||
|
if (securePolicy != other.securePolicy) return false
|
||||||
|
if (usePlatformDefaultWidth != other.usePlatformDefaultWidth) return false
|
||||||
|
if (decorFitsSystemWindows != other.decorFitsSystemWindows) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = dismissOnBackPress.hashCode()
|
||||||
|
result = 31 * result + dismissOnClickOutside.hashCode()
|
||||||
|
result = 31 * result + securePolicy.hashCode()
|
||||||
|
result = 31 * result + usePlatformDefaultWidth.hashCode()
|
||||||
|
result = 31 * result + decorFitsSystemWindows.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Dialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
properties: DialogProperties = DialogProperties(),
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
|
val composition = rememberCompositionContext()
|
||||||
|
val currentContent by rememberUpdatedState(content)
|
||||||
|
val dialogId = rememberSaveable { UUID.randomUUID() }
|
||||||
|
val dialog = remember(view, density) {
|
||||||
|
DialogWrapper(
|
||||||
|
onDismissRequest,
|
||||||
|
properties,
|
||||||
|
view,
|
||||||
|
layoutDirection,
|
||||||
|
density,
|
||||||
|
dialogId
|
||||||
|
).apply {
|
||||||
|
setContent(composition) {
|
||||||
|
// TODO(b/159900354): draw a scrim and add margins around the Compose Dialog, and
|
||||||
|
// consume clicks so they can't pass through to the underlying UI
|
||||||
|
DialogLayout(
|
||||||
|
Modifier.semantics { dialog() },
|
||||||
|
) {
|
||||||
|
currentContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(dialog) {
|
||||||
|
// Set the dialog's window type to TYPE_APPLICATION_OVERLAY so it's compatible with compose overlays
|
||||||
|
dialog.window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
dialog.dismiss()
|
||||||
|
dialog.disposeComposition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
|
dialog.updateParameters(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = properties,
|
||||||
|
layoutDirection = layoutDirection
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogWindowProvider {
|
||||||
|
val window: Window
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ViewConstructor")
|
||||||
|
private class DialogLayout(
|
||||||
|
context: Context,
|
||||||
|
override val window: Window
|
||||||
|
) : AbstractComposeView(context), DialogWindowProvider {
|
||||||
|
|
||||||
|
private var content: @Composable () -> Unit by mutableStateOf({})
|
||||||
|
|
||||||
|
var usePlatformDefaultWidth = false
|
||||||
|
|
||||||
|
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setContent(parent: CompositionContext, content: @Composable () -> Unit) {
|
||||||
|
setParentCompositionContext(parent)
|
||||||
|
this.content = content
|
||||||
|
shouldCreateCompositionOnAttachedToWindow = true
|
||||||
|
createComposition()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun measureChild(
|
||||||
|
child: View?,
|
||||||
|
parentWidthMeasureSpec: Int,
|
||||||
|
parentHeightMeasureSpec: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
super.measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val displayWidth: Int
|
||||||
|
get() {
|
||||||
|
val density = context.resources.displayMetrics.density
|
||||||
|
return (context.resources.configuration.screenWidthDp * density).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val displayHeight: Int
|
||||||
|
get() {
|
||||||
|
val density = context.resources.displayMetrics.density
|
||||||
|
return (context.resources.configuration.screenHeightDp * density).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DialogWrapper(
|
||||||
|
private var onDismissRequest: () -> Unit,
|
||||||
|
private var properties: DialogProperties,
|
||||||
|
private val composeView: View,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
density: Density,
|
||||||
|
dialogId: UUID
|
||||||
|
) : ComponentDialog(
|
||||||
|
ContextThemeWrapper(
|
||||||
|
composeView.context,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || properties.decorFitsSystemWindows) {
|
||||||
|
R.style.DialogWindowTheme
|
||||||
|
} else {
|
||||||
|
R.style.FloatingDialogWindowTheme
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ViewRootForInspector {
|
||||||
|
|
||||||
|
private val dialogLayout: DialogLayout
|
||||||
|
|
||||||
|
// On systems older than Android S, there is a bug in the surface insets matrix math used by
|
||||||
|
// elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
|
||||||
|
private val maxSupportedElevation = 8.dp
|
||||||
|
|
||||||
|
override val subCompositionView: AbstractComposeView get() = dialogLayout
|
||||||
|
|
||||||
|
private val defaultSoftInputMode: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
val window = window ?: error("Dialog has no window")
|
||||||
|
defaultSoftInputMode =
|
||||||
|
window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST
|
||||||
|
window.requestFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
window.setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
|
||||||
|
dialogLayout = DialogLayout(context, window).apply {
|
||||||
|
// Set unique id for AbstractComposeView. This allows state restoration for the state
|
||||||
|
// defined inside the Dialog via rememberSaveable()
|
||||||
|
setTag(R.id.compose_view_saveable_id_tag, "Dialog:$dialogId")
|
||||||
|
// Enable children to draw their shadow by not clipping them
|
||||||
|
clipChildren = false
|
||||||
|
// Allocate space for elevation
|
||||||
|
with(density) { elevation = maxSupportedElevation.toPx() }
|
||||||
|
// Simple outline to force window manager to allocate space for shadow.
|
||||||
|
// Note that the outline affects clickable area for the dismiss listener. In case of
|
||||||
|
// shapes like circle the area for dismiss might be to small (rectangular outline
|
||||||
|
// consuming clicks outside of the circle).
|
||||||
|
outlineProvider = object : ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, result: Outline) {
|
||||||
|
result.setRect(0, 0, view.width, view.height)
|
||||||
|
// We set alpha to 0 to hide the view's shadow and let the composable to draw
|
||||||
|
// its own shadow. This still enables us to get the extra space needed in the
|
||||||
|
// surface.
|
||||||
|
result.alpha = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables clipping for [this] and all its descendant [ViewGroup]s until we reach a
|
||||||
|
* [DialogLayout] (the [ViewGroup] containing the Compose hierarchy).
|
||||||
|
*/
|
||||||
|
fun ViewGroup.disableClipping() {
|
||||||
|
clipChildren = false
|
||||||
|
if (this is DialogLayout) return
|
||||||
|
for (i in 0 until childCount) {
|
||||||
|
(getChildAt(i) as? ViewGroup)?.disableClipping()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn of all clipping so shadows can be drawn outside the window
|
||||||
|
(window.decorView as? ViewGroup)?.disableClipping()
|
||||||
|
setContentView(dialogLayout)
|
||||||
|
dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
|
||||||
|
dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
|
||||||
|
dialogLayout.setViewTreeSavedStateRegistryOwner(
|
||||||
|
composeView.findViewTreeSavedStateRegistryOwner()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
updateParameters(onDismissRequest, properties, layoutDirection)
|
||||||
|
|
||||||
|
// Due to how the onDismissRequest callback works
|
||||||
|
// (it enforces a just-in-time decision on whether to update the state to hide the dialog)
|
||||||
|
// we need to unconditionally add a callback here that is always enabled,
|
||||||
|
// meaning we'll never get a system UI controlled predictive back animation
|
||||||
|
// for these dialogs
|
||||||
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
|
if (properties.dismissOnBackPress) {
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLayoutDirection(layoutDirection: LayoutDirection) {
|
||||||
|
dialogLayout.layoutDirection = when (layoutDirection) {
|
||||||
|
LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
|
||||||
|
LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(b/159900354): Make the Android Dialog full screen and the scrim fully transparent
|
||||||
|
|
||||||
|
fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) {
|
||||||
|
dialogLayout.setContent(parentComposition, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSecurePolicy(securePolicy: SecureFlagPolicy) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateParameters(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
properties: DialogProperties,
|
||||||
|
layoutDirection: LayoutDirection
|
||||||
|
) {
|
||||||
|
this.onDismissRequest = onDismissRequest
|
||||||
|
this.properties = properties
|
||||||
|
setSecurePolicy(properties.securePolicy)
|
||||||
|
setLayoutDirection(layoutDirection)
|
||||||
|
if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
|
||||||
|
// Undo fixed size in internalOnLayout, which would suppress size changes when
|
||||||
|
// usePlatformDefaultWidth is true.
|
||||||
|
window?.setLayout(
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
if (properties.decorFitsSystemWindows) {
|
||||||
|
window?.setSoftInputMode(defaultSoftInputMode)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disposeComposition() {
|
||||||
|
dialogLayout.disposeComposition()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
val result = super.onTouchEvent(event)
|
||||||
|
if (result && properties.dismissOnClickOutside) {
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
// Prevents the dialog from dismissing itself
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DialogLayout(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Layout(
|
||||||
|
content = content,
|
||||||
|
modifier = modifier
|
||||||
|
) { measurables, constraints ->
|
||||||
|
val placeables = measurables.map { it.measure(constraints) }
|
||||||
|
val width = placeables.maxBy { it.width }.width
|
||||||
|
val height = placeables.maxBy { it.height }.height
|
||||||
|
layout(width, height) {
|
||||||
|
placeables.forEach { it.placeRelative(0, 0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
app/src/main/res/values/styles.xml
Normal file
14
app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="FullscreenOverlayDialog">
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
<item name="android:windowFrame">@null</item>
|
||||||
|
<item name="android:windowIsFloating">false</item>
|
||||||
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
|
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
|
||||||
|
<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
|
||||||
|
<item name="android:windowActionBar">false</item>
|
||||||
|
<item name="android:windowActionModeOverlay">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
@ -93,4 +93,8 @@ interface BridgeInterface {
|
|||||||
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends);
|
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends);
|
||||||
|
|
||||||
IScripting getScriptingInterface();
|
IScripting getScriptingInterface();
|
||||||
|
|
||||||
|
void openSettingsOverlay();
|
||||||
|
|
||||||
|
void closeSettingsOverlay();
|
||||||
}
|
}
|
@ -25,7 +25,7 @@
|
|||||||
"downloads": "Downloads",
|
"downloads": "Downloads",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"home_debug": "Debug",
|
"home_settings": "Settings",
|
||||||
"home_logs": "Logs",
|
"home_logs": "Logs",
|
||||||
"social": "Social",
|
"social": "Social",
|
||||||
"scripts": "Scripts"
|
"scripts": "Scripts"
|
||||||
|
@ -16,12 +16,20 @@ import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
|
|||||||
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
|
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
|
||||||
import me.rhunk.snapenhance.core.util.ktx.getApplicationInfoCompat
|
import me.rhunk.snapenhance.core.util.ktx.getApplicationInfoCompat
|
||||||
import me.rhunk.snapenhance.data.SnapClassCache
|
import me.rhunk.snapenhance.data.SnapClassCache
|
||||||
|
import me.rhunk.snapenhance.hook.HookAdapter
|
||||||
import me.rhunk.snapenhance.hook.HookStage
|
import me.rhunk.snapenhance.hook.HookStage
|
||||||
import me.rhunk.snapenhance.hook.Hooker
|
import me.rhunk.snapenhance.hook.Hooker
|
||||||
import me.rhunk.snapenhance.hook.hook
|
import me.rhunk.snapenhance.hook.hook
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
import kotlin.time.measureTime
|
import kotlin.time.measureTime
|
||||||
|
|
||||||
|
private fun useMainActivity(hookAdapter: HookAdapter, block: Activity.() -> Unit) {
|
||||||
|
val activity = hookAdapter.thisObject() as Activity
|
||||||
|
if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return
|
||||||
|
block(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SnapEnhance {
|
class SnapEnhance {
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var classLoader: ClassLoader
|
lateinit var classLoader: ClassLoader
|
||||||
@ -68,12 +76,18 @@ class SnapEnhance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Activity::class.java.hook( "onCreate", HookStage.AFTER, { isBridgeInitialized }) {
|
Activity::class.java.hook( "onCreate", HookStage.AFTER, { isBridgeInitialized }) {
|
||||||
val activity = it.thisObject() as Activity
|
useMainActivity(it) {
|
||||||
if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook
|
val isMainActivityNotNull = appContext.mainActivity != null
|
||||||
val isMainActivityNotNull = appContext.mainActivity != null
|
appContext.mainActivity = this
|
||||||
appContext.mainActivity = activity
|
if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded()) return@useMainActivity
|
||||||
if (isMainActivityNotNull || !appContext.mappings.isMappingsLoaded()) return@hook
|
onActivityCreate()
|
||||||
onActivityCreate()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity::class.java.hook( "onPause", HookStage.AFTER, { isBridgeInitialized }) {
|
||||||
|
useMainActivity(it) {
|
||||||
|
appContext.bridgeClient.closeSettingsOverlay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityWasResumed = false
|
var activityWasResumed = false
|
||||||
@ -81,17 +95,16 @@ class SnapEnhance {
|
|||||||
//we need to reload the config when the app is resumed
|
//we need to reload the config when the app is resumed
|
||||||
//FIXME: called twice at first launch
|
//FIXME: called twice at first launch
|
||||||
Activity::class.java.hook("onResume", HookStage.AFTER, { isBridgeInitialized }) {
|
Activity::class.java.hook("onResume", HookStage.AFTER, { isBridgeInitialized }) {
|
||||||
val activity = it.thisObject() as Activity
|
useMainActivity(it) {
|
||||||
if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook
|
if (!activityWasResumed) {
|
||||||
|
activityWasResumed = true
|
||||||
|
return@useMainActivity
|
||||||
|
}
|
||||||
|
|
||||||
if (!activityWasResumed) {
|
appContext.actionManager.onNewIntent(this.intent)
|
||||||
activityWasResumed = true
|
appContext.reloadConfig()
|
||||||
return@hook
|
syncRemote()
|
||||||
}
|
}
|
||||||
|
|
||||||
appContext.actionManager.onNewIntent(activity.intent)
|
|
||||||
appContext.reloadConfig()
|
|
||||||
syncRemote()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,4 +140,7 @@ class BridgeClient(
|
|||||||
= service.setRule(targetUuid, type.key, state)
|
= service.setRule(targetUuid, type.key, state)
|
||||||
|
|
||||||
fun getScriptingInterface(): IScripting = service.getScriptingInterface()
|
fun getScriptingInterface(): IScripting = service.getScriptingInterface()
|
||||||
|
|
||||||
|
fun openSettingsOverlay() = service.openSettingsOverlay()
|
||||||
|
fun closeSettingsOverlay() = service.closeSettingsOverlay()
|
||||||
}
|
}
|
||||||
|
@ -45,11 +45,12 @@ class SettingsGearInjector : AbstractMenu() {
|
|||||||
isClickable = true
|
isClickable = true
|
||||||
|
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val intent = Intent().apply {
|
/* val intent = Intent().apply {
|
||||||
setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.ui.manager.MainActivity")
|
setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.ui.manager.MainActivity")
|
||||||
putExtra("route", "features")
|
putExtra("route", "features")
|
||||||
}
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)*/
|
||||||
|
this@SettingsGearInjector.context.bridgeClient.openSettingsOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.setOnTouchListener { _, event ->
|
parent.setOnTouchListener { _, event ->
|
||||||
|
Reference in New Issue
Block a user