Standardized sub-page layouts and enhanced system stability across all modules
This commit is contained in:
@@ -132,7 +132,7 @@ class MainActivity : ComponentActivity() {
|
||||
if (shouldShowAbiWarning) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = {},
|
||||
title = managerContext.translation["wrong_apk_title"],
|
||||
title = managerContext.translation["setup.activity.wrong_apk_title"],
|
||||
text = "",
|
||||
icon = Icons.Filled.Warning,
|
||||
confirmButtonText = managerContext.translation["common.close"],
|
||||
@@ -140,11 +140,20 @@ class MainActivity : ComponentActivity() {
|
||||
showCloseButton = false,
|
||||
opaque = true,
|
||||
customContent = {
|
||||
Text(
|
||||
text = managerContext.translation["wrong_apk_message"],
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = managerContext.translation["setup.activity.wrong_apk_message"],
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 20.sp,
|
||||
fontSize = 15.sp,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -163,7 +172,7 @@ class MainActivity : ComponentActivity() {
|
||||
insetsController.isAppearanceLightNavigationBars = isLight
|
||||
}
|
||||
// Floating bottom bar height and vertical spacing so floating action buttons and scrolling content remain readable:
|
||||
val bottomPadding = 80.dp + 16.dp +
|
||||
val bottomPadding = 82.dp + 4.dp +
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
routes.bottomPadding = bottomPadding
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -72,6 +73,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.setValue
|
||||
@@ -122,22 +124,36 @@ class Navigation(
|
||||
) {
|
||||
private val translation by lazy { context.translation.getCategory("manager.navigation") }
|
||||
var openBottomBarCustomization by mutableStateOf(false)
|
||||
var globalScrollOffset by mutableIntStateOf(0)
|
||||
|
||||
@Composable
|
||||
fun TopBar() {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = remember(navBackStackEntry) { routes.getCurrentRoute(navBackStackEntry) }
|
||||
if (currentRoute?.routeInfo?.hasOwnTopBar == true) return
|
||||
|
||||
val shrinkThreshold = 300f
|
||||
val focusFactor = (globalScrollOffset / shrinkThreshold).coerceIn(0f, 1f)
|
||||
val headerHeight = lerp(64.dp, 48.dp, focusFactor)
|
||||
|
||||
val canGoBack = remember(navBackStackEntry) {
|
||||
currentRoute?.let { !it.routeInfo.primary || it.routeInfo.childIds.contains(routes.currentDestination) } == true
|
||||
}
|
||||
TopAppBar(
|
||||
modifier = Modifier.height(headerHeight),
|
||||
title = {
|
||||
currentRoute?.apply {
|
||||
title?.invoke() ?: routeInfo.translatedKey?.value?.let {
|
||||
Text(
|
||||
text = it,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.graphicsLayer {
|
||||
// Title stays visible but scales slightly down
|
||||
scaleX = 1f - (focusFactor * 0.05f)
|
||||
scaleY = 1f - (focusFactor * 0.05f)
|
||||
translationY = (-2 * focusFactor).dp.toPx()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -146,7 +162,11 @@ class Navigation(
|
||||
val backButtonAnimation by animateFloatAsState(if (canGoBack) 1f else 0f, label = "backButton")
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.graphicsLayer { alpha = backButtonAnimation }
|
||||
.graphicsLayer {
|
||||
alpha = backButtonAnimation
|
||||
scaleX = 1f - (focusFactor * 0.1f)
|
||||
scaleY = 1f - (focusFactor * 0.1f)
|
||||
}
|
||||
.width(lerp(0.dp, 48.dp, backButtonAnimation))
|
||||
.height(48.dp)
|
||||
) {
|
||||
@@ -177,6 +197,13 @@ class Navigation(
|
||||
listOf(routes.tasks, routes.features, routes.home, routes.social, routes.scripting, routes.friendTracker)
|
||||
}
|
||||
val availableRouteMap = remember(availableRoutes) { availableRoutes.associateBy { it.routeInfo.id } }
|
||||
|
||||
val shrinkThreshold = 300f
|
||||
val focusFactor = (globalScrollOffset / shrinkThreshold).coerceIn(0f, 1f)
|
||||
val barHeight = lerp(82.dp, 64.dp, focusFactor)
|
||||
val labelAlpha = (1f - (focusFactor * 2.5f)).coerceIn(0f, 1f)
|
||||
val iconTranslationY = (10 * focusFactor).dp // Sinks towards vertical center
|
||||
|
||||
val prefs = remember { context.sharedPreferences }
|
||||
val defaultOrder = remember { listOf("tasks", "features", "home", "social", "scripts") }
|
||||
fun loadSelected(): List<String> {
|
||||
@@ -233,11 +260,20 @@ class Navigation(
|
||||
val animatedBarWidth by animateDpAsState(targetValue = targetBarWidth ?: 0.dp, label = "barWidth")
|
||||
Surface(
|
||||
shape = barShape,
|
||||
color = Color.Transparent,
|
||||
color = Color.White.copy(alpha = 0.08f), // Restored frosted glass harmony
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f)),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.9f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.85f)
|
||||
)
|
||||
)
|
||||
),
|
||||
modifier = Modifier
|
||||
.then(if (targetBarWidth != null) Modifier.width(animatedBarWidth) else Modifier.fillMaxWidth())
|
||||
.height(barHeight)
|
||||
.drawBehind {
|
||||
val radius = size.width * 0.62f
|
||||
drawCircle(
|
||||
@@ -263,7 +299,7 @@ class Navigation(
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(82.dp)
|
||||
.height(barHeight)
|
||||
.clip(barShape)
|
||||
.background(PurrfectPalette.cardOverlay)
|
||||
.border(BorderStroke(1.dp, barBorder), barShape)
|
||||
@@ -296,7 +332,7 @@ class Navigation(
|
||||
)
|
||||
}
|
||||
)
|
||||
Box(Modifier.fillMaxWidth().height(82.dp)) {
|
||||
Box(Modifier.fillMaxWidth().height(barHeight)) {
|
||||
var barWidthPx by remember { mutableStateOf(0f) }
|
||||
val itemCount = selectedRoutes.size.coerceAtLeast(1)
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
@@ -369,7 +405,7 @@ class Navigation(
|
||||
.fillMaxHeight()
|
||||
.width(indicatorWidth.coerceAtLeast(0.dp))
|
||||
.offset(x = offsetX)
|
||||
.padding(vertical = 10.dp, horizontal = 2.dp)
|
||||
.padding(vertical = lerp(10.dp, 8.dp, focusFactor), horizontal = 2.dp)
|
||||
.graphicsLayer { scaleX = scaleXAnim; scaleY = scaleYAnim }
|
||||
) {
|
||||
Box(
|
||||
@@ -430,7 +466,10 @@ class Navigation(
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(22.dp + 2.dp * selectionProgress)
|
||||
.graphicsLayer { alpha = 0.65f + 0.35f * selectionProgress }
|
||||
.graphicsLayer {
|
||||
alpha = 0.65f + 0.35f * selectionProgress
|
||||
translationY = iconTranslationY.toPx()
|
||||
}
|
||||
)
|
||||
},
|
||||
label = {
|
||||
@@ -441,11 +480,15 @@ class Navigation(
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White.copy(alpha = 0.6f + 0.4f * selectionProgress),
|
||||
color = Color.White.copy(alpha = (0.6f + 0.4f * selectionProgress) * labelAlpha),
|
||||
maxLines = if (isLong) 2 else 1,
|
||||
overflow = if (isLong) TextOverflow.Ellipsis else TextOverflow.Clip,
|
||||
softWrap = isLong,
|
||||
modifier = if (isLong) Modifier.widthIn(max = 90.dp).wrapContentWidth(Alignment.CenterHorizontally) else Modifier.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
modifier = (if (isLong) Modifier.widthIn(max = 90.dp).wrapContentWidth(Alignment.CenterHorizontally) else Modifier.wrapContentWidth(Alignment.CenterHorizontally))
|
||||
.graphicsLayer {
|
||||
alpha = labelAlpha
|
||||
translationY = (-10 * focusFactor).dp.toPx() // Fall into icon
|
||||
}
|
||||
)
|
||||
},
|
||||
selected = isSelected,
|
||||
|
||||
@@ -87,12 +87,12 @@ object Updater {
|
||||
}.getOrNull()
|
||||
|
||||
private fun fetchLatestDebugCI() = runCatching {
|
||||
val actionRuns = OkHttpClient().newCall(Request.Builder().url("https://api.github.com/repos/particle-box/PurrfectSnap/actions/runs?event=workflow_dispatch&branch=dev").build()).execute().use {
|
||||
val actionRuns = OkHttpClient().newCall(Request.Builder().url("https://api.github.com/repos/particle-box/PurrfectSnap/actions/runs?branch=dev&status=success").build()).execute().use {
|
||||
if (!it.isSuccessful) throw Throwable("Failed to fetch CI runs: ${it.code}")
|
||||
JsonParser.parseString(it.body?.string()).asJsonObject
|
||||
}
|
||||
val debugRuns = actionRuns.getAsJsonArray("workflow_runs")?.mapNotNull { it.asJsonObject }?.filter { run ->
|
||||
run.get("conclusion")?.takeIf { it.isJsonPrimitive }?.asString == "success" && run.getAsJsonPrimitive("path")?.asString == ".github/workflows/debug.yml"
|
||||
run.getAsJsonPrimitive("name")?.asString == "PurrfectSnap Debug CI"
|
||||
} ?: throw Throwable("No debug CI runs found")
|
||||
|
||||
val latestRun = debugRuns.firstOrNull() ?: throw Throwable("No debug CI runs found")
|
||||
@@ -109,15 +109,22 @@ object Updater {
|
||||
AbstractLogger.directError("Failed to fetch latest debug CI", it)
|
||||
}.getOrNull()
|
||||
|
||||
private val cache = mutableMapOf<Channel, LatestRelease?>()
|
||||
private val cache = mutableMapOf<Channel, Pair<Long, LatestRelease?>>()
|
||||
|
||||
fun getLatestRelease(channel: Channel): LatestRelease? {
|
||||
return cache.getOrPut(channel) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
fetchLatestDebugCI() ?: fetchLatestRelease(channel)
|
||||
} else {
|
||||
fetchLatestRelease(channel)
|
||||
}
|
||||
val cached = cache[channel]
|
||||
// Use 24-hour TTL (Time To Live) for cache to optimize API calls
|
||||
if (cached != null && (System.currentTimeMillis() - cached.first) < 24 * 60 * 60 * 1000) {
|
||||
return cached.second
|
||||
}
|
||||
|
||||
val result = if (channel == Channel.PRERELEASE) {
|
||||
fetchLatestDebugCI() ?: fetchLatestRelease(channel)
|
||||
} else {
|
||||
fetchLatestRelease(channel)
|
||||
}
|
||||
|
||||
cache[channel] = System.currentTimeMillis() to result
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.net.Uri
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -38,7 +38,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -157,8 +157,8 @@ class FileImportsRoot: Routes.Route() {
|
||||
.padding(horizontal = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
contentPadding = PaddingValues(
|
||||
top = topBarHeight + 8.dp,
|
||||
bottom = routes.bottomPadding + 16.dp
|
||||
top = topBarHeight,
|
||||
bottom = routes.bottomPadding
|
||||
)
|
||||
) {
|
||||
item {
|
||||
@@ -270,9 +270,7 @@ class FileImportsRoot: Routes.Route() {
|
||||
title = titleText ?: translation["import_file_button"],
|
||||
subtitle = null,
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
topBarHeight = with(density) { it.size.height.toDp() }
|
||||
}
|
||||
modifier = Modifier.headerHeightTracker { topBarHeight = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
@@ -38,6 +39,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -289,64 +292,137 @@ class LoggerHistoryRoot : Routes.Route() {
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
FloatingTopBar(
|
||||
title = context.translation["manager.routes.logger_history"] ?: "Logger History",
|
||||
onBack = { routes.navController.popBackStack() }
|
||||
)
|
||||
val listState = rememberLazyListState()
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp)) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(listState.firstVisibleItemScrollOffset, listState.firstVisibleItemIndex) {
|
||||
val offset = if (listState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else listState.firstVisibleItemScrollOffset
|
||||
routes.navigation?.globalScrollOffset = offset
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp, bottom = 10.dp),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Column(
|
||||
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp)) {
|
||||
Spacer(modifier = Modifier.height(controlsHeight))
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, RoundedCornerShape(22.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp, bottom = 10.dp),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, RoundedCornerShape(22.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
fun formatConversationInfo(conversationInfo: ConversationInfo?): String? {
|
||||
if (conversationInfo == null) return null
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
) {
|
||||
fun formatConversationInfo(conversationInfo: ConversationInfo?): String? {
|
||||
if (conversationInfo == null) return null
|
||||
|
||||
return conversationInfo.groupTitle?.let {
|
||||
translation.format("list_group_format", "name" to it)
|
||||
} ?: conversationInfo.usernames.takeIf { it.size > 1 }?.let {
|
||||
translation.format("list_friend_format", "name" to ("(" + it.joinToString(", ") + ")"))
|
||||
} ?: context.database.findFriend(conversationInfo.conversationId)?.let {
|
||||
translation.format("list_friend_format", "name" to "(" + (conversationInfo.usernames + listOf(it.mutableUsername)).toSet().joinToString(", ") + ")")
|
||||
} ?: conversationInfo.usernames.firstOrNull()?.let {
|
||||
translation.format("list_friend_format", "name" to "($it)")
|
||||
return conversationInfo.groupTitle?.let {
|
||||
translation.format("list_group_format", "name" to it)
|
||||
} ?: conversationInfo.usernames.takeIf { it.size > 1 }?.let {
|
||||
translation.format("list_friend_format", "name" to ("(" + it.joinToString(", ") + ")"))
|
||||
} ?: context.database.findFriend(conversationInfo.conversationId)?.let {
|
||||
translation.format("list_friend_format", "name" to "(" + (conversationInfo.usernames + listOf(it.mutableUsername)).toSet().joinToString(", ") + ")")
|
||||
} ?: conversationInfo.usernames.firstOrNull()?.let {
|
||||
translation.format("list_friend_format", "name" to "($it)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val selectedConversationInfo by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(selectedConversation)) {
|
||||
selectedConversation?.let {
|
||||
conversationInfoCache.getOrPut(it) {
|
||||
formatConversationInfo(loggerWrapper.getConversationInfo(it))
|
||||
val selectedConversationInfo by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(selectedConversation)) {
|
||||
selectedConversation?.let {
|
||||
conversationInfoCache.getOrPut(it) {
|
||||
formatConversationInfo(loggerWrapper.getConversationInfo(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = selectedConversationInfo ?: translation["select_conversation_placeholder"],
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
modifier = Modifier
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.06f),
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
cursorColor = PurrfectPalette.glowSecondary
|
||||
)
|
||||
)
|
||||
|
||||
val conversations by rememberAsyncMutableState(defaultValue = emptyList<String>()) {
|
||||
loggerWrapper.getAllConversations().toMutableList()
|
||||
}
|
||||
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
conversations.forEach { conversationId ->
|
||||
DropdownMenuItem(onClick = {
|
||||
selectedConversation = conversationId
|
||||
expanded = false
|
||||
}, text = {
|
||||
val conversationInfo by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(conversationId)) {
|
||||
conversationInfoCache.getOrPut(conversationId) {
|
||||
formatConversationInfo(loggerWrapper.getConversationInfo(conversationId))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = remember(conversationInfo) { conversationInfo ?: conversationId },
|
||||
fontWeight = if (conversationId == selectedConversation) FontWeight.Bold else FontWeight.Normal,
|
||||
color = Color.White,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = selectedConversationInfo ?: translation["select_conversation_placeholder"],
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
modifier = Modifier
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth(),
|
||||
value = stringFilter,
|
||||
onValueChange = { stringFilter = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search",
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = null,
|
||||
tint = PurrfectPalette.textSecondary
|
||||
)
|
||||
},
|
||||
trailingIcon = if (stringFilter.isNotBlank()) {
|
||||
{
|
||||
IconButton(onClick = { stringFilter = "" }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = translation["close_button_description"],
|
||||
tint = PurrfectPalette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
@@ -358,160 +434,99 @@ class LoggerHistoryRoot : Routes.Route() {
|
||||
)
|
||||
)
|
||||
|
||||
val conversations by rememberAsyncMutableState(defaultValue = emptyList<String>()) {
|
||||
loggerWrapper.getAllConversations().toMutableList()
|
||||
}
|
||||
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
conversations.forEach { conversationId ->
|
||||
DropdownMenuItem(onClick = {
|
||||
selectedConversation = conversationId
|
||||
expanded = false
|
||||
}, text = {
|
||||
val conversationInfo by rememberAsyncMutableState(defaultValue = null, keys = arrayOf(conversationId)) {
|
||||
conversationInfoCache.getOrPut(conversationId) {
|
||||
formatConversationInfo(loggerWrapper.getConversationInfo(conversationId))
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = remember(conversationInfo) { conversationInfo ?: conversationId },
|
||||
fontWeight = if (conversationId == selectedConversation) FontWeight.Bold else FontWeight.Normal,
|
||||
color = Color.White,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
})
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
translation["reverse_order_checkbox"],
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Checkbox(
|
||||
checked = reverseOrder,
|
||||
onCheckedChange = { reverseOrder = it },
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = PurrfectPalette.glowPrimary,
|
||||
checkmarkColor = Color.White,
|
||||
uncheckedColor = Color.White.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = stringFilter,
|
||||
onValueChange = { stringFilter = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search",
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Search,
|
||||
contentDescription = null,
|
||||
tint = PurrfectPalette.textSecondary
|
||||
)
|
||||
},
|
||||
trailingIcon = if (stringFilter.isNotBlank()) {
|
||||
{
|
||||
IconButton(onClick = { stringFilter = "" }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = translation["close_button_description"],
|
||||
tint = PurrfectPalette.textSecondary
|
||||
var hasReachedEnd by remember(selectedConversation, stringFilter, reverseOrder) { mutableStateOf(false) }
|
||||
var lastFetchMessageTimestamp by remember(selectedConversation, stringFilter, reverseOrder) { mutableLongStateOf(if (reverseOrder) Long.MAX_VALUE else Long.MIN_VALUE) }
|
||||
val messages = remember(selectedConversation, stringFilter, reverseOrder) { mutableStateListOf<LoggedMessage>() }
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(bottom = routes.bottomPadding)
|
||||
) {
|
||||
items(messages) { message ->
|
||||
MessageView(message)
|
||||
}
|
||||
item {
|
||||
if (selectedConversation != null) {
|
||||
if (hasReachedEnd) {
|
||||
Text(translation["no_more_messages"], modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.White)
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.padding(8.dp),
|
||||
color = PurrfectPalette.glowSecondary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.08f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.06f),
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
cursorColor = PurrfectPalette.glowSecondary
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
translation["reverse_order_checkbox"],
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
Checkbox(
|
||||
checked = reverseOrder,
|
||||
onCheckedChange = { reverseOrder = it },
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = PurrfectPalette.glowPrimary,
|
||||
checkmarkColor = Color.White,
|
||||
uncheckedColor = Color.White.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasReachedEnd by remember(selectedConversation, stringFilter, reverseOrder) { mutableStateOf(false) }
|
||||
var lastFetchMessageTimestamp by remember(selectedConversation, stringFilter, reverseOrder) { mutableLongStateOf(if (reverseOrder) Long.MAX_VALUE else Long.MIN_VALUE) }
|
||||
val messages = remember(selectedConversation, stringFilter, reverseOrder) { mutableStateListOf<LoggedMessage>() }
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(bottom = routes.bottomPadding)
|
||||
) {
|
||||
items(messages) { message ->
|
||||
MessageView(message)
|
||||
}
|
||||
item {
|
||||
if (selectedConversation != null) {
|
||||
if (hasReachedEnd) {
|
||||
Text(translation["no_more_messages"], modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.padding(8.dp),
|
||||
color = PurrfectPalette.glowSecondary,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit, selectedConversation, stringFilter, reverseOrder) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newMessages = loggerWrapper.fetchMessages(
|
||||
selectedConversation ?: return@withContext,
|
||||
lastFetchMessageTimestamp,
|
||||
30,
|
||||
reverseOrder
|
||||
) { messageData ->
|
||||
if (stringFilter.isEmpty()) return@fetchMessages true
|
||||
var isMatch = false
|
||||
decodeMessage(messageData) { contentType, messageReader, _ ->
|
||||
if (contentType == ContentType.CHAT) {
|
||||
val content = messageReader.getString(2, 1) ?: return@decodeMessage
|
||||
isMatch = content.contains(stringFilter, ignoreCase = true)
|
||||
LaunchedEffect(Unit, selectedConversation, stringFilter, reverseOrder) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newMessages = loggerWrapper.fetchMessages(
|
||||
selectedConversation ?: return@withContext,
|
||||
lastFetchMessageTimestamp,
|
||||
30,
|
||||
reverseOrder
|
||||
) { messageData ->
|
||||
if (stringFilter.isEmpty()) return@fetchMessages true
|
||||
var isMatch = false
|
||||
decodeMessage(messageData) { contentType, messageReader, _ ->
|
||||
if (contentType == ContentType.CHAT) {
|
||||
val content = messageReader.getString(2, 1) ?: return@decodeMessage
|
||||
isMatch = content.contains(stringFilter, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
isMatch
|
||||
}
|
||||
if (newMessages.isEmpty()) {
|
||||
hasReachedEnd = true
|
||||
return@withContext
|
||||
}
|
||||
lastFetchMessageTimestamp = newMessages.lastOrNull()?.sendTimestamp ?: return@withContext
|
||||
withContext(Dispatchers.Main) {
|
||||
messages.addAll(newMessages)
|
||||
}
|
||||
isMatch
|
||||
}
|
||||
if (newMessages.isEmpty()) {
|
||||
hasReachedEnd = true
|
||||
return@withContext
|
||||
}
|
||||
lastFetchMessageTimestamp = newMessages.lastOrNull()?.sendTimestamp ?: return@withContext
|
||||
withContext(Dispatchers.Main) {
|
||||
messages.addAll(newMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingTopBar(
|
||||
title = context.translation["manager.routes.logger_history"] ?: "Logger History",
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = listState.firstVisibleItemScrollOffset + (listState.firstVisibleItemIndex * Motion.HEADER_MORPH_THRESHOLD.toInt()),
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||
package me.eternal.purrfectsnap.ui.manager.pages
|
||||
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -32,6 +43,7 @@ import me.eternal.purrfectsnap.storage.addRepo
|
||||
import me.eternal.purrfectsnap.storage.getRepositories
|
||||
import me.eternal.purrfectsnap.storage.removeRepo
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class ManageReposSection: Routes.Route() {
|
||||
@@ -165,44 +177,74 @@ class ManageReposSection: Routes.Route() {
|
||||
val repositories = rememberAsyncMutableStateList(defaultValue = listOf(), updateDispatcher = updateDispatcher) {
|
||||
context.database.getRepositories(repoType)
|
||||
}
|
||||
val listState = rememberLazyListState()
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp + routes.bottomPadding),
|
||||
LaunchedEffect(listState.firstVisibleItemScrollOffset, listState.firstVisibleItemIndex) {
|
||||
val offset = if (listState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else listState.firstVisibleItemScrollOffset
|
||||
routes.navigation?.globalScrollOffset = offset
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
item {
|
||||
if (repositories.isEmpty()) {
|
||||
Text(translation["no_repos_added"], modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(), fontSize = 15.sp, fontWeight = FontWeight.Light, textAlign = TextAlign.Center)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(start = 8.dp, top = controlsHeight, end = 8.dp, bottom = routes.bottomPadding),
|
||||
) {
|
||||
item {
|
||||
if (repositories.isEmpty()) {
|
||||
Text(translation["no_repos_added"], modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.padding(top = 40.dp)
|
||||
.fillMaxWidth(), fontSize = 15.sp, fontWeight = FontWeight.Light, textAlign = TextAlign.Center, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
items(repositories) { url ->
|
||||
ElevatedCard(onClick = {
|
||||
context.androidContext.copyToClipboard(url)
|
||||
}, modifier = Modifier.animateContentSize()) {
|
||||
Row(
|
||||
items(repositories) { url ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
.clickable { context.androidContext.copyToClipboard(url) },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = Color.White.copy(alpha = 0.05f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||
) {
|
||||
Icon(Icons.Default.Public, contentDescription = null)
|
||||
Text(text = url, modifier = Modifier.weight(1f), overflow = TextOverflow.Ellipsis, maxLines = 4, fontSize = 15.sp, lineHeight = 15.sp)
|
||||
Button(
|
||||
onClick = {
|
||||
context.database.removeRepo(repoType, url)
|
||||
coroutineScope.launch {
|
||||
updateDispatcher.dispatch()
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(translation["remove_button"])
|
||||
Icon(Icons.Default.Public, contentDescription = null, tint = Color.White)
|
||||
Text(text = url, color = Color.White, modifier = Modifier.weight(1f), overflow = TextOverflow.Ellipsis, maxLines = 1, fontSize = 14.sp)
|
||||
Button(
|
||||
onClick = {
|
||||
context.database.removeRepo(repoType, url)
|
||||
coroutineScope.launch {
|
||||
updateDispatcher.dispatch()
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.White.copy(alpha = 0.1f), contentColor = Color.White)
|
||||
) {
|
||||
Text(translation["remove_button"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar(
|
||||
title = remember(repoType) { translation.format("title", "type" to repoType) },
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = listState.firstVisibleItemScrollOffset + (listState.firstVisibleItemIndex * Motion.HEADER_MORPH_THRESHOLD.toInt()),
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
@@ -41,6 +41,8 @@ import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -68,6 +70,7 @@ import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.OnLifecycleEvent
|
||||
import me.eternal.purrfectsnap.ui.util.coil.cacheKey
|
||||
import me.eternal.purrfectsnap.ui.util.scaleOnPress
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlin.math.absoluteValue
|
||||
@@ -105,7 +108,6 @@ class TasksRootSection : Routes.Route() {
|
||||
runCatching {
|
||||
pendingTask.updateProgress("Copying ${documentFile.name}")
|
||||
context.androidContext.contentResolver.openInputStream(documentFile.uri)?.use { inputStream ->
|
||||
//copy with progress
|
||||
val length = documentFile.length().toFloat()
|
||||
tempFile.outputStream().use { outputStream ->
|
||||
val buffer = ByteArray(16 * 1024)
|
||||
@@ -301,7 +303,7 @@ class TasksRootSection : Routes.Route() {
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = context.translation["delete_files_option"],
|
||||
text = context.translation["delete_files_option"] ?: "Delete Files",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@@ -389,59 +391,7 @@ class TasksRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
override val topBarActions: @Composable (RowScope.() -> Unit) = {
|
||||
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
if (taskSelection.size > 1) {
|
||||
val canMergeSelection by rememberAsyncMutableState(defaultValue = false, keys = arrayOf(taskSelection.size)) {
|
||||
taskSelection.all { it.second?.type?.contains("video") == true }
|
||||
}
|
||||
|
||||
if (canMergeSelection) {
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
mergeSelection(taskSelection.toList().also {
|
||||
taskSelection.clear()
|
||||
}.map { it.first to it.second!! })
|
||||
},
|
||||
icon = Icons.Filled.Merge,
|
||||
text = translation["merge_button"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
showConfirmDialog = true
|
||||
}) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = translation["clear_button_description"])
|
||||
}
|
||||
|
||||
if (showConfirmDialog) {
|
||||
var alsoDeleteFiles by remember { mutableStateOf(false) }
|
||||
val isSelection = taskSelection.isNotEmpty()
|
||||
val titleText = if (isSelection) {
|
||||
translation.format("remove_selected_tasks_confirm", "count" to taskSelection.size.toString())
|
||||
} else {
|
||||
translation["remove_all_tasks_confirm"]
|
||||
}
|
||||
val messageText = if (isSelection) translation["remove_selected_tasks_title"] else translation["remove_all_tasks_title"]
|
||||
|
||||
TaskDangerDialog(
|
||||
visible = showConfirmDialog,
|
||||
title = titleText,
|
||||
message = messageText,
|
||||
showDeleteFiles = isSelection,
|
||||
deleteFilesChecked = alsoDeleteFiles,
|
||||
onToggleDeleteFiles = { alsoDeleteFiles = it },
|
||||
onConfirm = {
|
||||
showConfirmDialog = false
|
||||
clearTasks(alsoDeleteFiles, coroutineScope)
|
||||
},
|
||||
onDismiss = { showConfirmDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
override val topBarActions: @Composable (RowScope.() -> Unit) = {}
|
||||
|
||||
@Composable
|
||||
private fun TaskCard(modifier: Modifier, task: Task, pendingTask: PendingTask? = null) {
|
||||
@@ -613,7 +563,7 @@ class TasksRootSection : Routes.Route() {
|
||||
} else {
|
||||
when {
|
||||
!isDocumentFileReadable -> Icon(Icons.Filled.DeleteOutline, contentDescription = "File not found")
|
||||
documentFileMimeType.contains("image") -> Icon(Icons.Filled.Image, contentDescription = "Image")
|
||||
documentFileMimeType.contains("image") -> Icon(Icons.Filled.Photo, contentDescription = "Image")
|
||||
documentFileMimeType.contains("video") -> Icon(Icons.Filled.Videocam, contentDescription = "Video")
|
||||
documentFileMimeType.contains("audio") -> Icon(Icons.Filled.MusicNote, contentDescription = "Audio")
|
||||
else -> Icon(Icons.Filled.FileCopy, contentDescription = "File")
|
||||
@@ -674,11 +624,9 @@ class TasksRootSection : Routes.Route() {
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
if (task.type == TaskType.SCHEDULED_SEND) {
|
||||
// Professional design for scheduled send tasks
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
// Feature title
|
||||
Text(
|
||||
context.translation.getOrNull("scheduled_send_title") ?: "Scheduled Snaps",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
@@ -686,7 +634,6 @@ class TasksRootSection : Routes.Route() {
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
|
||||
)
|
||||
|
||||
// Scheduled time without icon
|
||||
Text(
|
||||
task.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@@ -694,7 +641,6 @@ class TasksRootSection : Routes.Route() {
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
// Recipients with icon
|
||||
task.author?.takeIf { it != "null" }?.let { recipients ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
@@ -844,6 +790,14 @@ class TasksRootSection : Routes.Route() {
|
||||
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val scrollState = rememberLazyListState()
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
LaunchedEffect(scrollState.firstVisibleItemScrollOffset, scrollState.firstVisibleItemIndex) {
|
||||
val offset = if (scrollState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else scrollState.firstVisibleItemScrollOffset
|
||||
routes.navigation?.globalScrollOffset = offset
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
recentTasks = remember { mutableStateListOf() }
|
||||
var lastFetchedTaskId by remember { mutableStateOf(null as Long?) }
|
||||
@@ -883,151 +837,98 @@ class TasksRootSection : Routes.Route() {
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
color = Color.White.copy(alpha = 0.07f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
val subtitle = if (activeTasks.isNotEmpty()) {
|
||||
translation.format(
|
||||
"summary_active",
|
||||
"active" to activeTasks.size.toString(),
|
||||
"recent" to recentTasks.size.toString()
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
text = context.translation["manager.routes.tasks"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Text(
|
||||
text = if (activeTasks.isNotEmpty()) {
|
||||
translation.format(
|
||||
"summary_active",
|
||||
"active" to activeTasks.size.toString(),
|
||||
"recent" to recentTasks.size.toString()
|
||||
)
|
||||
} else {
|
||||
translation.format(
|
||||
"summary_idle",
|
||||
"recent" to recentTasks.size.toString()
|
||||
)
|
||||
},
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (taskSelection.size > 1 && taskSelection.all { it.second?.type?.contains("video") == true }) {
|
||||
Surface(
|
||||
onClick = {
|
||||
} else {
|
||||
translation.format(
|
||||
"summary_idle",
|
||||
"recent" to recentTasks.size.toString()
|
||||
)
|
||||
}
|
||||
|
||||
me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar(
|
||||
title = context.translation["manager.routes.tasks"] ?: "Tasks",
|
||||
subtitle = subtitle,
|
||||
scrollOffset = if (scrollState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else scrollState.firstVisibleItemScrollOffset,
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||
actions = {
|
||||
if (taskSelection.size > 1 && taskSelection.all { it.second?.type?.contains("video") == true }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(50),
|
||||
color = Color.White.copy(alpha = 0.1f),
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.clickable {
|
||||
mergeSelection(
|
||||
taskSelection.toList().also { taskSelection.clear() }
|
||||
.map { it.first to it.second!! }
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(listOf(PurrfectPalette.glowPrimary, PurrfectPalette.glowSecondary))
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(Icons.Filled.Merge, contentDescription = translation["merge_button"], tint = Color.White)
|
||||
Text(translation["merge_button"], color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.16f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Filled.PlaylistAddCheckCircle, contentDescription = null, tint = Color.White)
|
||||
Icon(
|
||||
Icons.Filled.Merge,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
text = translation.format("running_count", "count" to activeTasks.size.toString()),
|
||||
translation["merge_button"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 12.sp
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { showConfirmDialog = true }) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = translation["clear_button_description"], tint = Color.White)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
if (taskSelection.isEmpty()) {
|
||||
showConfirmDialog = true
|
||||
} else {
|
||||
showConfirmDialog = true
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Filled.DeleteSweep, contentDescription = translation["clear_button_description"], tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = controlsHeight,
|
||||
bottom = routes.bottomPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 0.dp,
|
||||
bottom = routes.bottomPadding + 16.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
if (activeTasks.isEmpty() && recentTasks.isEmpty()) {
|
||||
TasksEmptyState(text = translation["no_tasks"])
|
||||
}
|
||||
item {
|
||||
if (activeTasks.isEmpty() && recentTasks.isEmpty()) {
|
||||
TasksEmptyState(text = translation["no_tasks"])
|
||||
}
|
||||
items(activeTasks, key = { it.taskId }) { pendingTask ->
|
||||
TaskCard(modifier = Modifier.fillMaxWidth(), pendingTask.task, pendingTask = pendingTask)
|
||||
}
|
||||
items(recentTasks, key = { it.hash }) { task ->
|
||||
TaskCard(modifier = Modifier.fillMaxWidth(), task)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
LaunchedEffect(remember { derivedStateOf { scrollState.firstVisibleItemIndex } }) {
|
||||
fetchNewRecentTasks()
|
||||
}
|
||||
}
|
||||
items(activeTasks, key = { it.taskId }) { pendingTask ->
|
||||
TaskCard(modifier = Modifier.fillMaxWidth(), pendingTask.task, pendingTask = pendingTask)
|
||||
}
|
||||
items(recentTasks, key = { it.hash }) { task ->
|
||||
TaskCard(modifier = Modifier.fillMaxWidth(), task)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
LaunchedEffect(remember { derivedStateOf { scrollState.firstVisibleItemIndex } }) {
|
||||
fetchNewRecentTasks()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1045,8 +946,8 @@ class TasksRootSection : Routes.Route() {
|
||||
|
||||
TaskDangerDialog(
|
||||
visible = showConfirmDialog,
|
||||
title = titleText,
|
||||
message = messageText,
|
||||
title = titleText ?: "",
|
||||
message = messageText ?: "",
|
||||
showDeleteFiles = isSelection,
|
||||
deleteFilesChecked = alsoDeleteFiles,
|
||||
onToggleDeleteFiles = { alsoDeleteFiles = it },
|
||||
|
||||
@@ -1,45 +1,24 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.features
|
||||
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -50,7 +29,9 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.saveFile
|
||||
import me.eternal.purrfectsnap.storage.getLocationCoordinates
|
||||
import org.json.JSONArray
|
||||
@@ -147,189 +128,102 @@ class ConfigExportSummaryScreen : Routes.Route() {
|
||||
parser.parse(context.config.exportToString(exportSensitiveData, includeSavedLocations, savedLocations))
|
||||
}
|
||||
val expandedState = remember { mutableStateMapOf<String, Boolean>() }
|
||||
val listState = rememberLazyListState()
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Bottom))
|
||||
.padding(horizontal = 12.dp),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
top = controlsHeight,
|
||||
bottom = routes.bottomPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 12.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
items(featuresByCategory.toList()) { (category, features) ->
|
||||
val isExpanded = expandedState[category] ?: false
|
||||
val rotationState by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = { routes.navController.popBackStack() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.28f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
Text(context.translation["common.back"])
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = translation["title"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
routes.activityLauncher.saveFile("config.json", "application/json") { uri ->
|
||||
runCatching {
|
||||
context.androidContext.contentResolver.openOutputStream(android.net.Uri.parse(uri))?.use {
|
||||
context.config.writeConfig()
|
||||
context.config.exportToString(exportSensitiveData, includeSavedLocations, savedLocations).byteInputStream().copyTo(it)
|
||||
context.shortToast(context.translation["manager.sections.features.config_export_success_toast"])
|
||||
}
|
||||
}.onFailure {
|
||||
context.longToast(
|
||||
context.translation.format(
|
||||
"manager.sections.features.config_export_failure_toast",
|
||||
"error" to it.message.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.32f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDownward,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
Text(exportLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 16.dp + routes.bottomPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
items(featuresByCategory.toList()) { (category, features) ->
|
||||
val isExpanded = expandedState[category] ?: false
|
||||
val rotationState by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expandedState[category] = !isExpanded },
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.4f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.32f)
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.clickable { expandedState[category] = !isExpanded },
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.4f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.32f)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = category,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 17.sp,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { expandedState[category] = !isExpanded }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = translation["expand_button_description"],
|
||||
modifier = Modifier.graphicsLayer(rotationZ = rotationState),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = category,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 17.sp,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
features.forEachIndexed { index, feature ->
|
||||
when (val parsedValue = parser.parseValue(feature.key, feature.value)) {
|
||||
is List<*> -> {
|
||||
IconButton(onClick = { expandedState[category] = !isExpanded }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = translation["expand_button_description"],
|
||||
modifier = Modifier.graphicsLayer(rotationZ = rotationState),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
features.forEachIndexed { index, feature ->
|
||||
when (val parsedValue = parser.parseValue(feature.key, feature.value)) {
|
||||
is List<*> -> {
|
||||
Column(
|
||||
modifier = Modifier.padding(start = (feature.indentation * 16).dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = feature.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(start = (feature.indentation * 16).dp),
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = feature.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
parsedValue.forEachIndexed { itemIndex, item ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NumberBubble(itemIndex + 1)
|
||||
Text(
|
||||
text = item.toString(),
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
}
|
||||
parsedValue.forEachIndexed { itemIndex, item ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NumberBubble(itemIndex + 1)
|
||||
Text(
|
||||
text = item.toString(),
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is String -> {
|
||||
Column(
|
||||
@@ -354,7 +248,6 @@ class ConfigExportSummaryScreen : Routes.Route() {
|
||||
if (index < features.size - 1) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,6 +255,35 @@ class ConfigExportSummaryScreen : Routes.Route() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingTopBar(
|
||||
title = translation["title"],
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = listState.firstVisibleItemScrollOffset + (listState.firstVisibleItemIndex * Motion.HEADER_MORPH_THRESHOLD.toInt()),
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
routes.activityLauncher.saveFile("config.json", "application/json") { uri ->
|
||||
runCatching {
|
||||
context.androidContext.contentResolver.openOutputStream(android.net.Uri.parse(uri))?.use {
|
||||
context.config.writeConfig()
|
||||
context.config.exportToString(exportSensitiveData, includeSavedLocations, savedLocations).byteInputStream().copyTo(it)
|
||||
context.shortToast(context.translation["manager.sections.features.config_export_success_toast"])
|
||||
}
|
||||
}.onFailure {
|
||||
context.longToast(
|
||||
context.translation.format(
|
||||
"manager.sections.features.config_export_failure_toast",
|
||||
"error" to it.message.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(imageVector = Icons.Default.ArrowDownward, contentDescription = exportLabel, tint = Color.White)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.features
|
||||
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -53,7 +34,9 @@ import me.eternal.purrfectsnap.bridge.location.LocationCoordinates
|
||||
import me.eternal.purrfectsnap.storage.addOrUpdateLocationCoordinate
|
||||
import me.eternal.purrfectsnap.storage.getLocationCoordinates
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import kotlin.math.abs
|
||||
@@ -73,10 +56,6 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
private const val COORDINATE_TOLERANCE = 0.0001 // ~11 meters tolerance for de-duplication
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports saved locations from JSON array into database with de-duplication.
|
||||
* Only adds locations that don't already exist (within coordinate tolerance).
|
||||
*/
|
||||
private fun importSavedLocations(locationsArray: com.google.gson.JsonArray) {
|
||||
val existingLocations = context.database.getLocationCoordinates()
|
||||
|
||||
@@ -87,14 +66,12 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
val longitude = locationObj.get("longitude")?.asDouble ?: continue
|
||||
val radius = locationObj.get("radius")?.asDouble ?: 100.0
|
||||
|
||||
// Check for existing location with similar coordinates (de-duplication)
|
||||
val existingMatch = existingLocations.find { existing ->
|
||||
abs(existing.latitude - latitude) < COORDINATE_TOLERANCE &&
|
||||
abs(existing.longitude - longitude) < COORDINATE_TOLERANCE
|
||||
}
|
||||
|
||||
if (existingMatch == null) {
|
||||
// No duplicate found, add as new location
|
||||
val newLocation = LocationCoordinates().apply {
|
||||
this.name = name
|
||||
this.latitude = latitude
|
||||
@@ -103,7 +80,6 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
}
|
||||
context.database.addOrUpdateLocationCoordinate(null, newLocation)
|
||||
}
|
||||
// If duplicate exists, skip (do not update or delete existing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,111 +209,26 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
}
|
||||
val expandedState = remember { mutableStateMapOf<String, Boolean>() }
|
||||
val importLabel = translation["confirm_button"]
|
||||
val listState = rememberLazyListState()
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 12.dp),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
top = controlsHeight,
|
||||
bottom = routes.bottomPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 12.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = { routes.navController.popBackStack() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.28f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
Text(context.translation["common.back"])
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = translation["title"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
routes.configJsonForImport?.let { json ->
|
||||
runCatching {
|
||||
val savedLocationsJson = context.config.loadFromString(json)
|
||||
|
||||
// Import saved locations if present in the JSON
|
||||
savedLocationsJson?.let { locationsArray ->
|
||||
importSavedLocations(locationsArray)
|
||||
}
|
||||
}.onFailure { err ->
|
||||
context.longToast(
|
||||
context.translation.format(
|
||||
"config_import_failure_toast",
|
||||
"error" to (err.message ?: context.translation["common.unknown_error"])
|
||||
)
|
||||
)
|
||||
}
|
||||
context.shortToast(translation["config_imported_toast"])
|
||||
context.coroutineScope.launch(Dispatchers.Main) {
|
||||
routes.features.navigateReload()
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PurrfectPalette.glowPrimary.copy(alpha = 0.3f),
|
||||
contentColor = Color.White
|
||||
)
|
||||
) {
|
||||
Text(importLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 16.dp + routes.bottomPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
items(featuresByCategory.toList()) { (category, features) ->
|
||||
val isExpanded = expandedState[category] ?: false
|
||||
val rotationState by animateFloatAsState(targetValue = if (isExpanded) 180f else 0f)
|
||||
@@ -439,15 +330,46 @@ class ConfigImportConfirmationScreen : Routes.Route() {
|
||||
}
|
||||
if (index < features.size - 1) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingTopBar(
|
||||
title = translation["title"],
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = listState.firstVisibleItemScrollOffset + (listState.firstVisibleItemIndex * Motion.HEADER_MORPH_THRESHOLD.toInt()),
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
routes.configJsonForImport?.let { json ->
|
||||
runCatching {
|
||||
val savedLocationsJson = context.config.loadFromString(json)
|
||||
savedLocationsJson?.let { locationsArray ->
|
||||
importSavedLocations(locationsArray)
|
||||
}
|
||||
}.onFailure { err ->
|
||||
context.longToast(
|
||||
context.translation.format(
|
||||
"config_import_failure_toast",
|
||||
"error" to (err.message ?: context.translation["common.unknown_error"])
|
||||
)
|
||||
)
|
||||
}
|
||||
context.shortToast(translation["config_imported_toast"])
|
||||
context.coroutineScope.launch(Dispatchers.Main) {
|
||||
routes.features.navigateReload()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Icon(imageVector = Icons.Default.Check, contentDescription = importLabel, tint = Color.White)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -48,6 +47,7 @@ import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.social.AddFriendDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.pages.social.AddFriendDialog.Actions
|
||||
|
||||
@@ -223,25 +223,17 @@ class ManageRuleFeature : Routes.Route() {
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
var topBarHeight by remember { mutableStateOf(96.dp) }
|
||||
FloatingTopBar(
|
||||
title = remember { context.translation[propertyKeyPair.key.propertyName()] },
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
modifier = Modifier
|
||||
.zIndex(2f)
|
||||
.onGloballyPositioned {
|
||||
val newHeight = with(density) { it.size.height.toDp() }
|
||||
if (newHeight != topBarHeight) topBarHeight = newHeight
|
||||
}
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = topBarHeight + 10.dp)
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
val scrollState = rememberScrollState()
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(top = controlsHeight)
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
val headerShape = RoundedCornerShape(22.dp)
|
||||
@@ -397,6 +389,13 @@ class ManageRuleFeature : Routes.Route() {
|
||||
|
||||
Spacer(modifier = Modifier.height(routes.bottomPadding))
|
||||
}
|
||||
|
||||
FloatingTopBar(
|
||||
title = remember { context.translation[propertyKeyPair.key.propertyName()] },
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = scrollState.value,
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,19 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.home
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -44,6 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -57,9 +37,12 @@ import me.eternal.purrfectsnap.common.util.ktx.openLink
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import android.os.SystemClock
|
||||
import me.eternal.purrfectsnap.ui.util.PurrfectMarqueeText
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
|
||||
class HomeAbout : Routes.Route() {
|
||||
override val translation by lazy { context.translation.getCategory("manager.sections.home_about") }
|
||||
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val avenirNext = remember {
|
||||
FontFamily(Font(R.font.avenir_next_medium, FontWeight.Medium))
|
||||
@@ -67,9 +50,7 @@ class HomeAbout : Routes.Route() {
|
||||
val scrollState = rememberScrollState()
|
||||
val aboutStory = remember { translation["about_story"] }
|
||||
val pagePadding = 16.dp
|
||||
val bottomPadding = routes.bottomPadding +
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +
|
||||
24.dp
|
||||
val bottomPadding = routes.bottomPadding
|
||||
val tapSource = remember { MutableInteractionSource() }
|
||||
val tapTimeoutMs = 1500L
|
||||
val tapCount = remember { mutableIntStateOf(0) }
|
||||
@@ -79,24 +60,23 @@ class HomeAbout : Routes.Route() {
|
||||
context.shortToast(translation["about_magic_toast"])
|
||||
}
|
||||
|
||||
LaunchedEffect(scrollState.value) {
|
||||
routes.navigation?.globalScrollOffset = scrollState.value
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(bottom = bottomPadding)
|
||||
.padding(top = controlsHeight, bottom = bottomPadding)
|
||||
) {
|
||||
FloatingTopBar(
|
||||
title = routeInfo.translatedKey?.value ?: translation["manager.routes.home_about"],
|
||||
onBack = { routes.navController.popBackStack() }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = pagePadding)
|
||||
@@ -156,13 +136,13 @@ class HomeAbout : Routes.Route() {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
DeveloperCard(
|
||||
name = "ΞTΞRNAL",
|
||||
name = translation["about_dev_external"],
|
||||
imageRes = R.drawable.pfp_external,
|
||||
avenirNext = avenirNext,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
DeveloperCard(
|
||||
name = "<RSR/>",
|
||||
name = translation["about_dev_rsr"],
|
||||
imageRes = R.drawable.pfp_rsr,
|
||||
avenirNext = avenirNext,
|
||||
modifier = Modifier.weight(1f)
|
||||
@@ -176,7 +156,8 @@ class HomeAbout : Routes.Route() {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = pagePadding)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = 500.dp),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
color = Color.Transparent,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f)),
|
||||
@@ -186,20 +167,24 @@ class HomeAbout : Routes.Route() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay)
|
||||
.padding(horizontal = 20.dp, vertical = 18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = translation["about_story_title"],
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = aboutStory,
|
||||
fontSize = 14.sp,
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 20.sp
|
||||
textAlign = TextAlign.Justify,
|
||||
lineHeight = 22.sp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -278,9 +263,14 @@ class HomeAbout : Routes.Route() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
|
||||
FloatingTopBar(
|
||||
title = context.translation["manager.routes.home_about"] ?: "About Us",
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = scrollState.value,
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,10 +306,10 @@ class HomeAbout : Routes.Route() {
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(82.dp)
|
||||
.size(76.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.1f))
|
||||
.border(2.dp, imageRing, CircleShape)
|
||||
.border(1.5.dp, imageRing, CircleShape)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = imageRes),
|
||||
@@ -328,16 +318,17 @@ class HomeAbout : Routes.Route() {
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
Text(
|
||||
PurrfectMarqueeText(
|
||||
text = name,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = avenirNext
|
||||
),
|
||||
color = Color.White,
|
||||
fontFamily = avenirNext,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,11 @@ import me.eternal.purrfectsnap.LogReader
|
||||
import me.eternal.purrfectsnap.common.logger.LogChannel
|
||||
import me.eternal.purrfectsnap.common.logger.LogLevel
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
import me.eternal.purrfectsnap.ui.util.pullrefresh.PullRefreshIndicator
|
||||
import me.eternal.purrfectsnap.ui.util.pullrefresh.pullRefresh
|
||||
import me.eternal.purrfectsnap.ui.util.pullrefresh.rememberPullRefreshState
|
||||
@@ -73,9 +76,9 @@ import me.eternal.purrfectsnap.ui.util.saveFile
|
||||
import me.eternal.purrfectsnap.common.util.ktx.copyToClipboard
|
||||
|
||||
class HomeLogs : Routes.Route() {
|
||||
private val logListState by lazy { LazyListState(0) }
|
||||
private val logListState = LazyListState()
|
||||
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
||||
private val externalRefreshTick = mutableStateOf(0)
|
||||
private val externalRefreshTick = mutableIntStateOf(0)
|
||||
override val init: () -> Unit = {
|
||||
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||
}
|
||||
@@ -109,18 +112,19 @@ class HomeLogs : Routes.Route() {
|
||||
override val topBarActions: @Composable (RowScope.() -> Unit) = {}
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
val composeContext = LocalContext.current
|
||||
var logReader by remember { mutableStateOf<LogReader?>(null) }
|
||||
val visibleLogs = remember { mutableStateListOf<LogLine>() }
|
||||
val mainExecutor = remember { context.androidContext.mainExecutor }
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
||||
fun refreshLogs() {
|
||||
coroutineScope.launch {
|
||||
val readerResult = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
context.log.newReader { line ->
|
||||
if (shouldHideLog(line)) return@newReader
|
||||
mainExecutor.execute {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
visibleLogs.add(line)
|
||||
}
|
||||
}
|
||||
@@ -141,8 +145,7 @@ class HomeLogs : Routes.Route() {
|
||||
}
|
||||
delay(220)
|
||||
if (visibleLogs.isNotEmpty()) {
|
||||
val targetIndex = (visibleLogs.size - 1).coerceAtLeast(0)
|
||||
logListState.scrollToItem(targetIndex)
|
||||
logListState.scrollToItem((visibleLogs.size - 1).coerceAtLeast(0))
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
@@ -167,49 +170,98 @@ class HomeLogs : Routes.Route() {
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
.pullRefresh(pullRefreshState)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
LogsFloatingBar(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
isRefreshing = true
|
||||
refreshLogs()
|
||||
},
|
||||
onExport = { exportLogs() },
|
||||
onClear = { clearLogsAndReload() }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 12.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||
) {
|
||||
if (visibleLogs.isEmpty() && logReader != null) {
|
||||
EmptyLogsState()
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
state = logListState,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = 12.dp,
|
||||
bottom = routes.bottomPadding + 22.dp
|
||||
)
|
||||
) {
|
||||
items(visibleLogs, key = { it.hashCode() }) { line ->
|
||||
LogEntryCard(line = line, composeContext = composeContext)
|
||||
}
|
||||
var showDropDown by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 12.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||
) {
|
||||
if (visibleLogs.isEmpty() && logReader != null) {
|
||||
EmptyLogsState()
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
state = logListState,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = controlsHeight,
|
||||
bottom = routes.bottomPadding + 12.dp
|
||||
)
|
||||
) {
|
||||
items(visibleLogs, key = { it.hashCode() }) { line ->
|
||||
LogEntryCard(line = line, composeContext = composeContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingTopBar(
|
||||
title = context.translation["manager.routes.home_logs"] ?: "Logs",
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = if (logListState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else logListState.firstVisibleItemScrollOffset,
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||
actions = {
|
||||
if (isRefreshing) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { isRefreshing = true; refreshLogs() }) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Refresh", tint = Color.White)
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { showDropDown = true }) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = null, tint = Color.White)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showDropDown,
|
||||
onDismissRequest = { showDropDown = false },
|
||||
offset = DpOffset(0.dp, 8.dp),
|
||||
containerColor = Color(0xFF161821),
|
||||
tonalElevation = 8.dp,
|
||||
shadowElevation = 12.dp,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
clearLogsAndReload()
|
||||
showDropDown = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Filled.DeleteSweep, contentDescription = null, tint = PurrfectPalette.glowPrimary) },
|
||||
text = { Text(translation["clear_logs_button"], color = Color.White) },
|
||||
colors = MenuDefaults.itemColors(
|
||||
textColor = Color.White,
|
||||
leadingIconColor = PurrfectPalette.glowPrimary
|
||||
)
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
exportLogs()
|
||||
showDropDown = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Filled.Download, contentDescription = null, tint = PurrfectPalette.glowSecondary) },
|
||||
text = { Text(translation["export_logs_button"], color = Color.White) },
|
||||
colors = MenuDefaults.itemColors(
|
||||
textColor = Color.White,
|
||||
leadingIconColor = PurrfectPalette.glowSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = isRefreshing,
|
||||
state = pullRefreshState,
|
||||
@@ -270,116 +322,6 @@ class HomeLogs : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LogsFloatingBar(
|
||||
isRefreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
onExport: () -> Unit,
|
||||
onClear: () -> Unit
|
||||
) {
|
||||
var showDropDown by remember { mutableStateOf(false) }
|
||||
val topPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
.padding(top = topPadding),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = Color.White.copy(alpha = 0.07f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.6f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 18.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
IconButton(onClick = { routes.navController.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, tint = Color.White)
|
||||
}
|
||||
Text(
|
||||
text = routeInfo.translatedKey?.value ?: translation["manager.routes.home_logs"],
|
||||
color = PurrfectPalette.textPrimary,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
if (isRefreshing) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = PurrfectPalette.glowSecondary
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onRefresh) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Refresh", tint = Color.White)
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = { showDropDown = true }) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = null, tint = PurrfectPalette.glowSecondary)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showDropDown,
|
||||
onDismissRequest = { showDropDown = false },
|
||||
offset = DpOffset(0.dp, 8.dp),
|
||||
containerColor = Color(0xFF161821),
|
||||
tonalElevation = 8.dp,
|
||||
shadowElevation = 12.dp,
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onClear()
|
||||
showDropDown = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Filled.DeleteSweep, contentDescription = null, tint = PurrfectPalette.glowPrimary) },
|
||||
text = { Text(translation["clear_logs_button"], color = Color.White) },
|
||||
colors = MenuDefaults.itemColors(
|
||||
textColor = Color.White,
|
||||
leadingIconColor = PurrfectPalette.glowPrimary
|
||||
)
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onExport()
|
||||
showDropDown = false
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Filled.Download, contentDescription = null, tint = PurrfectPalette.glowSecondary) },
|
||||
text = { Text(translation["export_logs_button"], color = Color.White) },
|
||||
colors = MenuDefaults.itemColors(
|
||||
textColor = Color.White,
|
||||
leadingIconColor = PurrfectPalette.glowSecondary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyLogsState() {
|
||||
Column(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,7 @@ import me.eternal.purrfectsnap.task.UpdateCheckWorker
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.setup.Requirements
|
||||
import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper
|
||||
import me.eternal.purrfectsnap.ui.util.AlertDialogs
|
||||
@@ -203,7 +204,7 @@ class HomeSettings : Routes.Route() {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = text, modifier = Modifier.padding(start = 26.dp, end = 16.dp), fontSize = 14.sp)
|
||||
Text(text = text, modifier = Modifier.padding(start = 26.dp, end = 16.dp), fontSize = 14.sp, color = Color.White)
|
||||
Switch(
|
||||
checked = value,
|
||||
onCheckedChange = null,
|
||||
@@ -235,7 +236,7 @@ class HomeSettings : Routes.Route() {
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = text, modifier = Modifier.padding(start = 26.dp, end = 16.dp), fontSize = 14.sp)
|
||||
Text(text = text, modifier = Modifier.padding(start = 26.dp, end = 16.dp), fontSize = 14.sp, color = Color.White)
|
||||
Switch(
|
||||
checked = value,
|
||||
onCheckedChange = null,
|
||||
@@ -280,8 +281,8 @@ class HomeSettings : Routes.Route() {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(text = context.translation["actions.$key.name"], fontSize = 16.sp, fontWeight = FontWeight.Bold, lineHeight = 20.sp)
|
||||
context.translation.getOrNull("actions.$key.description")?.let { Text(text = it, fontSize = 12.sp, fontWeight = FontWeight.Light, lineHeight = 15.sp) }
|
||||
Text(text = context.translation["actions.$key.name"], fontSize = 16.sp, fontWeight = FontWeight.Bold, lineHeight = 20.sp, color = Color.White)
|
||||
context.translation.getOrNull("actions.$key.description")?.let { Text(text = it, fontSize = 12.sp, fontWeight = FontWeight.Light, lineHeight = 15.sp, color = PurrfectPalette.textSecondary) }
|
||||
}
|
||||
IconButton(onClick = { takeAction() },
|
||||
modifier = Modifier.padding(end = 2.dp)
|
||||
@@ -289,7 +290,8 @@ class HomeSettings : Routes.Route() {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
contentDescription = context.translation.getOrNull("actions.$key.name"),
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -313,6 +315,11 @@ class HomeSettings : Routes.Route() {
|
||||
val contextC = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(scrollState.value) {
|
||||
routes.navigation?.globalScrollOffset = scrollState.value
|
||||
}
|
||||
|
||||
val positiveLabel = context.translation["button.positive"]
|
||||
val negativeLabel = context.translation["button.negative"]
|
||||
val importLabel = context.translation["button.import"]
|
||||
@@ -372,109 +379,27 @@ class HomeSettings : Routes.Route() {
|
||||
}
|
||||
|
||||
val topPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
if (showResetSetupDialog) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showResetSetupDialog = false },
|
||||
title = translation["reset_setup_dialog_title"],
|
||||
text = translation["reset_setup_dialog_text"],
|
||||
icon = Icons.Filled.Warning,
|
||||
confirmButtonText = positiveLabel,
|
||||
dismissButtonText = negativeLabel,
|
||||
onConfirm = {
|
||||
showResetSetupDialog = false
|
||||
context.sharedPreferences.edit()
|
||||
.remove("setup_in_progress")
|
||||
.remove("setup_current_route")
|
||||
.remove("setup_skip_patch")
|
||||
.remove("setup_install_mode")
|
||||
.apply()
|
||||
|
||||
context.config.reset()
|
||||
context.config.writeConfig()
|
||||
|
||||
val intent = android.content.Intent(
|
||||
context.androidContext,
|
||||
me.eternal.purrfectsnap.ui.setup.SetupActivity::class.java
|
||||
)
|
||||
intent.flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.androidContext.startActivity(intent)
|
||||
routes.navController.popBackStack()
|
||||
},
|
||||
onDismiss = { showResetSetupDialog = false },
|
||||
showCloseButton = false
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(topPadding))
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
color = Color.White.copy(alpha = 0.07f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
),
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
IconButton(onClick = { routes.navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = translation["manager.routes.home_settings"],
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
IconButton(onClick = { routes.navigation?.openBottomBarCustomization = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Tune,
|
||||
contentDescription = null,
|
||||
tint = Color.White.copy(alpha = 0.85f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(top = controlsHeight, bottom = routes.bottomPadding)
|
||||
) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
GlassCard {
|
||||
RowTitle(title = translation["actions_title"])
|
||||
EnumAction.entries.forEach { enumAction ->
|
||||
@@ -486,56 +411,54 @@ class HomeSettings : Routes.Route() {
|
||||
|
||||
GlassCard {
|
||||
RowTitle(title = translation["ui_settings_title"])
|
||||
ShiftedRow {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["haptic_feedback_label"])
|
||||
var hapticFeedbackEnabled by remember { mutableStateOf(context.config.root.global.uiSettings.hapticFeedback.getNullable() ?: true) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = hapticFeedbackEnabled,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
hapticFeedbackEnabled = it
|
||||
context.config.root.global.uiSettings.hapticFeedback.set(it)
|
||||
context.config.writeConfig()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["use_system_toasts_label"])
|
||||
var useSystemToasts by remember { mutableStateOf(context.config.root.global.uiSettings.useSystemToasts.getNullable() ?: false) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = useSystemToasts,
|
||||
onCheckedChange = {
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
useSystemToasts = it
|
||||
context.config.root.global.uiSettings.useSystemToasts.set(it)
|
||||
context.config.writeConfig()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["haptic_feedback_label"], color = Color.White, modifier = Modifier.padding(start = 26.dp, end = 16.dp))
|
||||
var hapticFeedbackEnabled by remember { mutableStateOf(context.config.root.global.uiSettings.hapticFeedback.getNullable() ?: true) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = hapticFeedbackEnabled,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
hapticFeedbackEnabled = it
|
||||
context.config.root.global.uiSettings.hapticFeedback.set(it)
|
||||
context.config.writeConfig()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["use_system_toasts_label"], color = Color.White, modifier = Modifier.padding(start = 26.dp, end = 16.dp))
|
||||
var useSystemToasts by remember { mutableStateOf(context.config.root.global.uiSettings.useSystemToasts.getNullable() ?: false) }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = useSystemToasts,
|
||||
onCheckedChange = {
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
useSystemToasts = it
|
||||
context.config.root.global.uiSettings.useSystemToasts.set(it)
|
||||
context.config.writeConfig()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,31 +469,29 @@ class HomeSettings : Routes.Route() {
|
||||
var autoUpdateCheck by remember { mutableStateOf(context.config.root.global.updateSettings.autoUpdateCheck.getNullable() ?: true) }
|
||||
var selectedChannel by remember { mutableStateOf(context.config.root.global.updateSettings.updateChannel.getNullable() ?: "stable") }
|
||||
var channelMenuExpanded by remember { mutableStateOf(false) }
|
||||
ShiftedRow {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["auto_update_check"])
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = autoUpdateCheck,
|
||||
onCheckedChange = {
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
autoUpdateCheck = it
|
||||
context.config.root.global.updateSettings.autoUpdateCheck.set(it)
|
||||
context.config.writeConfig()
|
||||
scheduleUpdateCheck()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 55.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = translation["auto_update_check"], color = Color.White, modifier = Modifier.padding(start = 26.dp, end = 16.dp))
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
Switch(
|
||||
checked = autoUpdateCheck,
|
||||
onCheckedChange = {
|
||||
if (context.config.root.global.uiSettings.hapticFeedback.get()) {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
autoUpdateCheck = it
|
||||
context.config.root.global.updateSettings.autoUpdateCheck.set(it)
|
||||
context.config.writeConfig()
|
||||
scheduleUpdateCheck()
|
||||
},
|
||||
modifier = Modifier.padding(end = 26.dp),
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = autoUpdateCheck) {
|
||||
val spacingModifier = Modifier
|
||||
@@ -628,12 +549,14 @@ class HomeSettings : Routes.Route() {
|
||||
text = translation["reset_setup_action"],
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
lineHeight = 20.sp
|
||||
lineHeight = 20.sp,
|
||||
color = Color.White
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
contentDescription = translation["reset_setup_action"],
|
||||
modifier = Modifier.padding(end = 14.dp)
|
||||
modifier = Modifier.padding(end = 14.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -933,35 +856,80 @@ class HomeSettings : Routes.Route() {
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f)),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
Icon(Icons.Default.DeleteSweep, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Icon(Icons.Default.DeleteSweep, contentDescription = null, modifier = Modifier.size(18.dp), tint = Color.White)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(translation["clear_button"])
|
||||
}
|
||||
}
|
||||
ShiftedRow {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
PremiumPreferenceToggle(
|
||||
context.sharedPreferences,
|
||||
key = "test_mode",
|
||||
text = translation["test_mode_label"],
|
||||
defaultValue = true,
|
||||
confirmDisableTitle = translation["purr_aura_disable_title"],
|
||||
confirmDisableText = translation["purr_aura_disable_text"]
|
||||
)
|
||||
PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = translation["disable_feature_loading_label"])
|
||||
PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = translation["disable_auto_mapper_label"])
|
||||
PreferenceToggle(context.sharedPreferences, key = "disable_bypass_indicator", text = translation["disable_bypass_indicator_label"])
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
PremiumPreferenceToggle(
|
||||
context.sharedPreferences,
|
||||
key = "test_mode",
|
||||
text = translation["test_mode_label"],
|
||||
defaultValue = true,
|
||||
confirmDisableTitle = translation["purr_aura_disable_title"],
|
||||
confirmDisableText = translation["purr_aura_disable_text"]
|
||||
)
|
||||
PreferenceToggle(context.sharedPreferences, key = "disable_feature_loading", text = translation["disable_feature_loading_label"])
|
||||
PreferenceToggle(context.sharedPreferences, key = "disable_mapper", text = translation["disable_auto_mapper_label"])
|
||||
PreferenceToggle(context.sharedPreferences, key = "disable_bypass_indicator", text = translation["disable_bypass_indicator_label"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(routes.bottomPadding + 12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar(
|
||||
title = translation["manager.routes.home_settings"] ?: "Settings",
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = scrollState.value,
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||
actions = {
|
||||
IconButton(onClick = { routes.navigation?.openBottomBarCustomization = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Tune,
|
||||
contentDescription = null,
|
||||
tint = Color.White.copy(alpha = 0.85f)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (showResetSetupDialog) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showResetSetupDialog = false },
|
||||
title = translation["reset_setup_dialog_title"],
|
||||
text = translation["reset_setup_dialog_text"],
|
||||
icon = Icons.Filled.Warning,
|
||||
confirmButtonText = positiveLabel,
|
||||
dismissButtonText = negativeLabel,
|
||||
onConfirm = {
|
||||
showResetSetupDialog = false
|
||||
context.sharedPreferences.edit()
|
||||
.remove("setup_in_progress")
|
||||
.remove("setup_current_route")
|
||||
.remove("setup_skip_patch")
|
||||
.remove("setup_install_mode")
|
||||
.apply()
|
||||
|
||||
context.config.reset()
|
||||
context.config.writeConfig()
|
||||
|
||||
val intent = android.content.Intent(
|
||||
context.androidContext,
|
||||
me.eternal.purrfectsnap.ui.setup.SetupActivity::class.java
|
||||
)
|
||||
intent.flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.androidContext.startActivity(intent)
|
||||
routes.navController.popBackStack()
|
||||
},
|
||||
onDismiss = { showResetSetupDialog = false },
|
||||
showCloseButton = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticEmptyState
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class ManageScriptReposSection : Routes.Route() {
|
||||
@@ -296,10 +297,7 @@ class ManageScriptReposSection : Routes.Route() {
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
modifier = Modifier
|
||||
.zIndex(2f)
|
||||
.onGloballyPositioned {
|
||||
val newHeight = with(density) { it.size.height.toDp() }
|
||||
if (newHeight != topBarHeight) topBarHeight = newHeight
|
||||
}
|
||||
.headerHeightTracker { topBarHeight = it }
|
||||
)
|
||||
if (repositories.isEmpty()) {
|
||||
Box(
|
||||
@@ -319,9 +317,9 @@ class ManageScriptReposSection : Routes.Route() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 12.dp,
|
||||
top = topBarHeight + 12.dp,
|
||||
top = topBarHeight,
|
||||
end = 12.dp,
|
||||
bottom = 18.dp + routes.bottomPadding
|
||||
bottom = routes.bottomPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -22,12 +23,15 @@ import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
@@ -38,6 +42,7 @@ import me.eternal.purrfectsnap.common.scripting.ui.InterfaceManager
|
||||
import me.eternal.purrfectsnap.common.scripting.ui.ScriptInterface
|
||||
import me.eternal.purrfectsnap.common.ui.AsyncUpdateDispatcher
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableStateList
|
||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncUpdateDispatcher
|
||||
import me.eternal.purrfectsnap.common.util.ktx.getUrlFromClipboard
|
||||
import me.eternal.purrfectsnap.common.util.ktx.openLink
|
||||
@@ -47,6 +52,8 @@ import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticEmptyState
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper
|
||||
import me.eternal.purrfectsnap.ui.util.Dialog
|
||||
import me.eternal.purrfectsnap.ui.util.chooseFolder
|
||||
@@ -59,7 +66,6 @@ class ScriptingRootSection : Routes.Route() {
|
||||
override val translation by lazy { context.translation.getCategory("manager.scripting") }
|
||||
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
||||
val reloadDispatcher = AsyncUpdateDispatcher(updateOnFirstComposition = false)
|
||||
private var selectedTab by mutableStateOf(0)
|
||||
|
||||
override val init: () -> Unit = {
|
||||
activityLauncherHelper = ActivityLauncherHelper(context.activity!!)
|
||||
@@ -140,7 +146,7 @@ class ScriptingRootSection : Routes.Route() {
|
||||
return@launch
|
||||
}.onFailure {
|
||||
context.log.error("Failed to import script", it)
|
||||
context.shortToast(translation.format("import_failed", "message" to (it.message ?: context.translation["common.unknown"])))
|
||||
context.shortToast(translation.format("import_failed", "message" to (it.message ?: "Unknown")))
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
@@ -431,7 +437,7 @@ class ScriptingRootSection : Routes.Route() {
|
||||
|
||||
@Composable
|
||||
private fun SelectFolderButton(onClick: () -> Unit) {
|
||||
val label = translation["select_folder_button"]
|
||||
val label = translation.getOrNull("select_folder_button") ?: "Select folder"
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -439,13 +445,13 @@ class ScriptingRootSection : Routes.Route() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.size(78.dp),
|
||||
modifier = Modifier.size(68.dp),
|
||||
shape = CircleShape,
|
||||
color = PurrfectPalette.cardOverlayColor.copy(alpha = 0.9f),
|
||||
color = Color.White.copy(alpha = 0.1f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 14.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(
|
||||
1.5.dp,
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.7f),
|
||||
@@ -456,18 +462,17 @@ class ScriptingRootSection : Routes.Route() {
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.size(66.dp)
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.42f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.34f)
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.335f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.25f),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
.border(1.dp, Color.White.copy(alpha = 0.14f), CircleShape)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -475,7 +480,7 @@ class ScriptingRootSection : Routes.Route() {
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
||||
contentDescription = label,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(34.dp)
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -500,6 +505,7 @@ class ScriptingRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||
override val content: @Composable (androidx.navigation.NavBackStackEntry) -> Unit = {
|
||||
val scriptingFolder by rememberAsyncMutableState(
|
||||
defaultValue = null,
|
||||
@@ -508,10 +514,14 @@ class ScriptingRootSection : Routes.Route() {
|
||||
val tabTitles = listOf(translation["installed_scripts_tab"], translation["catalog_tab"])
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
var showToast by remember { mutableStateOf(false) }
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = androidx.compose.foundation.pager.rememberPagerState { tabTitles.size }
|
||||
|
||||
LaunchedEffect(scriptingFolder) {
|
||||
if (scriptingFolder == null && selectedTab != 0) {
|
||||
selectedTab = 0
|
||||
if (scriptingFolder == null && pagerState.currentPage != 0) {
|
||||
pagerState.scrollToPage(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,12 +542,12 @@ class ScriptingRootSection : Routes.Route() {
|
||||
) {
|
||||
ScriptingHeader(
|
||||
titles = tabTitles,
|
||||
selectedTab = selectedTab,
|
||||
selectedTab = pagerState.currentPage,
|
||||
onTabSelected = { index ->
|
||||
if (index == 1 && scriptingFolder == null) {
|
||||
showToast = true
|
||||
} else {
|
||||
selectedTab = index
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
}
|
||||
},
|
||||
onImport = {
|
||||
@@ -562,15 +572,99 @@ class ScriptingRootSection : Routes.Route() {
|
||||
context.translation["toast_open_link_failed"]
|
||||
)
|
||||
},
|
||||
folderSelected = scriptingFolder != null
|
||||
folderSelected = scriptingFolder != null,
|
||||
onPositioned = { controlsHeight = it }
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
when (selectedTab) {
|
||||
0 -> InstalledTabContent(
|
||||
scriptingFolder = scriptingFolder
|
||||
)
|
||||
1 -> CatalogTabContent(
|
||||
scriptingFolder = scriptingFolder
|
||||
|
||||
androidx.compose.foundation.pager.HorizontalPager(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = pagerState,
|
||||
userScrollEnabled = scriptingFolder != null
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> InstalledTabContent(
|
||||
scriptingFolder = scriptingFolder,
|
||||
controlsHeight = controlsHeight
|
||||
)
|
||||
1 -> CatalogTabContent(
|
||||
scriptingFolder = scriptingFolder,
|
||||
controlsHeight = controlsHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var scriptingWarning by remember {
|
||||
mutableStateOf<Boolean>(context.sharedPreferences.run {
|
||||
getBoolean("scripting_warning", true).also {
|
||||
if (it) edit().putBoolean("scripting_warning", false).apply()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (scriptingWarning) {
|
||||
var timeout by remember { mutableIntStateOf(10) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (timeout > 0) {
|
||||
delay(1000)
|
||||
timeout--
|
||||
}
|
||||
}
|
||||
AestheticDialog(
|
||||
onDismissRequest = { if (timeout == 0) scriptingWarning = false },
|
||||
title = context.translation["manager.dialogs.scripting_warning.title"] ?: "Scripting Warning",
|
||||
text = context.translation["manager.dialogs.scripting_warning.content"] ?: "Scripts can execute arbitrary code on your device. Only install scripts from trusted sources.",
|
||||
icon = Icons.Default.Warning,
|
||||
confirmButtonText = translation["button.ok"] ?: "OK",
|
||||
onConfirm = { if (timeout == 0) scriptingWarning = false },
|
||||
loading = timeout > 0,
|
||||
showCloseButton = false,
|
||||
customContent = {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.25f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.18f)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = timeout.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -578,7 +672,8 @@ class ScriptingRootSection : Routes.Route() {
|
||||
|
||||
@Composable
|
||||
private fun InstalledTabContent(
|
||||
scriptingFolder: DocumentFile?
|
||||
scriptingFolder: DocumentFile?,
|
||||
controlsHeight: androidx.compose.ui.unit.Dp
|
||||
) {
|
||||
val scriptModules by rememberAsyncMutableState(
|
||||
defaultValue = emptyList(),
|
||||
@@ -605,229 +700,144 @@ class ScriptingRootSection : Routes.Route() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemScrollOffset, listState.firstVisibleItemIndex) {
|
||||
val offset = if (listState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else listState.firstVisibleItemScrollOffset
|
||||
routes.navigation?.globalScrollOffset = offset
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pullRefresh(pullRefreshState),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(bottom = routes.bottomPadding, start = 8.dp, end = 8.dp, top = 12.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pullRefresh(pullRefreshState),
|
||||
contentPadding = PaddingValues(bottom = routes.bottomPadding + 28.dp, start = 8.dp, end = 8.dp, top = 12.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
if (scriptingFolder == null && !refreshing) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 260.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.White.copy(alpha = 0.05f),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.6f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
)
|
||||
item {
|
||||
if (scriptingFolder == null && !refreshing) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(260.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = Color.White.copy(alpha = 0.05f),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.6f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.45f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(58.dp)
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(Color.White.copy(alpha = 0.08f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderOpen,
|
||||
contentDescription = null,
|
||||
tint = PurrfectPalette.glowSecondary,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = translation["no_scripts_folder_selected_title"],
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = translation["select_scripts_folder_toast"],
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
SelectFolderButton(
|
||||
onClick = {
|
||||
activityLauncherHelper.chooseFolder {
|
||||
context.config.root.scripting.moduleFolder.set(it)
|
||||
context.config.writeConfig()
|
||||
coroutineScope.launch { reloadDispatcher.dispatch() }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (scriptModules.isEmpty()) {
|
||||
Box(
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(220.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
.padding(horizontal = 20.dp, vertical = 22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
AestheticEmptyState(
|
||||
icon = Icons.Default.DataObject,
|
||||
title = translation["no_scripts_found_title"],
|
||||
subtitle = translation["use_catalog_to_add_scripts"],
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 22.dp)
|
||||
.size(58.dp)
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(Color.White.copy(alpha = 0.08f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.FolderOpen,
|
||||
contentDescription = null,
|
||||
tint = PurrfectPalette.glowSecondary,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = translation["no_scripts_folder_selected_title"] ?: "No scripts folder selected",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
textAlign = TextAlign.Center,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = translation["select_scripts_folder_toast"] ?: "Please select a folder to store scripts.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = PurrfectPalette.textSecondary,
|
||||
lineHeight = 18.sp
|
||||
)
|
||||
SelectFolderButton(
|
||||
onClick = {
|
||||
activityLauncherHelper.chooseFolder {
|
||||
context.config.root.scripting.moduleFolder.set(it)
|
||||
context.config.writeConfig()
|
||||
coroutineScope.launch { reloadDispatcher.dispatch() }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
items(scriptModules.size, key = { scriptModules[it].hashCode() }) { index ->
|
||||
ModuleItem(scriptModules[index])
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
refreshing = refreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
var scriptingWarning by remember {
|
||||
mutableStateOf(context.sharedPreferences.run {
|
||||
getBoolean("scripting_warning", true).also {
|
||||
edit().putBoolean("scripting_warning", false).apply()
|
||||
}
|
||||
})
|
||||
}
|
||||
if (scriptingWarning) {
|
||||
var timeout by remember { mutableIntStateOf(10) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (timeout > 0) {
|
||||
delay(1000)
|
||||
timeout--
|
||||
}
|
||||
}
|
||||
AestheticDialog(
|
||||
onDismissRequest = { if (timeout == 0) scriptingWarning = false },
|
||||
title = context.translation["manager.dialogs.scripting_warning.title"],
|
||||
text = context.translation["manager.dialogs.scripting_warning.content"],
|
||||
icon = Icons.Default.Warning,
|
||||
confirmButtonText = translation["button.ok"],
|
||||
onConfirm = { if (timeout == 0) scriptingWarning = false },
|
||||
loading = timeout > 0,
|
||||
showCloseButton = false,
|
||||
customContent = {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
} else if (scriptModules.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
.height(220.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
AestheticEmptyState(
|
||||
icon = Icons.Default.DataObject,
|
||||
title = translation["no_scripts_found_title"] ?: "No scripts found",
|
||||
subtitle = translation["use_catalog_to_add_scripts"] ?: "Check the catalog to find and install scripts.",
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.25f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.18f)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = timeout.toString(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
items(scriptModules.size, key = { scriptModules[it].hashCode() }) { index ->
|
||||
ModuleItem(scriptModules[index])
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
refreshing = refreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CatalogTabContent(
|
||||
scriptingFolder: DocumentFile?
|
||||
scriptingFolder: DocumentFile?,
|
||||
controlsHeight: androidx.compose.ui.unit.Dp
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||
) {
|
||||
if (scriptingFolder == null) {
|
||||
AestheticEmptyState(
|
||||
icon = Icons.Default.FolderOpen,
|
||||
title = translation["no_scripts_folder_selected_title"],
|
||||
subtitle = translation["select_scripts_folder_toast"],
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 22.dp)
|
||||
)
|
||||
} else {
|
||||
ScriptCatalog(this@ScriptingRootSection)
|
||||
}
|
||||
if (scriptingFolder == null) {
|
||||
AestheticEmptyState(
|
||||
icon = Icons.Default.FolderOpen,
|
||||
title = translation["no_scripts_folder_selected_title"] ?: "No scripts folder selected",
|
||||
subtitle = translation["select_scripts_folder_toast"] ?: "Please select a folder to store scripts.",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 22.dp)
|
||||
)
|
||||
} else {
|
||||
ScriptCatalog(this@ScriptingRootSection)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -841,71 +851,51 @@ class ScriptingRootSection : Routes.Route() {
|
||||
onOpenFolder: () -> Unit,
|
||||
onManageRepos: () -> Unit,
|
||||
onDocs: () -> Unit,
|
||||
folderSelected: Boolean
|
||||
folderSelected: Boolean,
|
||||
onPositioned: (androidx.compose.ui.unit.Dp) -> Unit = {}
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
color = Color.White.copy(alpha = 0.07f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = translation["manager.routes.scripts"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
val scrollOffset = routes.navigation?.globalScrollOffset ?: 0
|
||||
val shrinkThreshold = 300f
|
||||
val focusFactor = (scrollOffset / shrinkThreshold).coerceIn(0f, 1f)
|
||||
val tabSwitcherAlpha = (1f - (focusFactor * 2.5f)).coerceIn(0f, 1f)
|
||||
|
||||
Column(modifier = Modifier.headerHeightTracker(onPositioned), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar(
|
||||
title = translation["manager.routes.scripts"] ?: "Scripts",
|
||||
subtitle = if (selectedTab == 0) translation["installed_scripts_tab"] else translation["catalog_tab"],
|
||||
scrollOffset = scrollOffset,
|
||||
actions = {
|
||||
IconButton(onClick = onDocs) {
|
||||
Icon(Icons.Default.CollectionsBookmark, contentDescription = translation["documentation_button"], tint = Color.White)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onDocs) {
|
||||
Icon(Icons.Default.CollectionsBookmark, contentDescription = translation["documentation_button"], tint = Color.White)
|
||||
}
|
||||
IconButton(onClick = onManageRepos) {
|
||||
Icon(Icons.Default.Public, contentDescription = translation["manage_repos_button"], tint = Color.White)
|
||||
}
|
||||
IconButton(onClick = onOpenFolder) {
|
||||
Icon(Icons.Default.FolderOpen, contentDescription = translation["open_scripts_folder_button"], tint = Color.White)
|
||||
}
|
||||
IconButton(onClick = onImport, enabled = folderSelected) {
|
||||
Icon(Icons.Default.Link, contentDescription = translation["import_from_url_button"], tint = if (folderSelected) Color.White else Color.White.copy(alpha = 0.4f))
|
||||
}
|
||||
IconButton(onClick = onManageRepos) {
|
||||
Icon(Icons.Default.Public, contentDescription = translation["manage_repos_button"], tint = Color.White)
|
||||
}
|
||||
IconButton(onClick = onOpenFolder) {
|
||||
Icon(Icons.Default.FolderOpen, contentDescription = translation["open_scripts_folder_button"], tint = Color.White)
|
||||
}
|
||||
IconButton(onClick = onImport, enabled = folderSelected) {
|
||||
Icon(Icons.Default.Link, contentDescription = translation["import_from_url_button"], tint = if (folderSelected) Color.White else Color.White.copy(alpha = 0.4f))
|
||||
}
|
||||
}
|
||||
ScriptingTabSwitcher(
|
||||
titles = titles,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = onTabSelected
|
||||
)
|
||||
)
|
||||
|
||||
if (tabSwitcherAlpha > 0.05f) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp)
|
||||
.graphicsLayer {
|
||||
alpha = tabSwitcherAlpha
|
||||
translationY = (-10 * focusFactor).dp.toPx()
|
||||
}
|
||||
) {
|
||||
ScriptingTabSwitcher(
|
||||
titles = titles,
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = onTabSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -956,5 +946,5 @@ class ScriptingRootSection : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
override val topBarActions: @Composable() (RowScope.() -> Unit) = {}
|
||||
override val topBarActions: @Composable RowScope.() -> Unit = {}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -47,6 +46,7 @@ import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.AlertDialogs
|
||||
import me.eternal.purrfectsnap.ui.util.Dialog
|
||||
import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors
|
||||
@@ -118,16 +118,13 @@ class ManageScope: Routes.Route() {
|
||||
},
|
||||
modifier = Modifier
|
||||
.zIndex(2f)
|
||||
.onGloballyPositioned {
|
||||
val newHeight = with(density) { it.size.height.toDp() }
|
||||
if (newHeight != topBarHeight) topBarHeight = newHeight
|
||||
}
|
||||
.headerHeightTracker { topBarHeight = it }
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = topBarHeight + 8.dp)
|
||||
.padding(top = topBarHeight)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
var bottomComposable by remember {
|
||||
|
||||
@@ -31,7 +31,6 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -59,6 +58,7 @@ import me.eternal.purrfectsnap.storage.getGroupInfo
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Dialog
|
||||
import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors
|
||||
|
||||
@@ -767,16 +767,13 @@ class MessagingPreview: Routes.Route() {
|
||||
},
|
||||
modifier = Modifier
|
||||
.zIndex(2f)
|
||||
.onGloballyPositioned {
|
||||
val newHeight = with(density) { it.size.height.toDp() }
|
||||
if (newHeight != topBarHeight) topBarHeight = newHeight
|
||||
}
|
||||
.headerHeightTracker { topBarHeight = it }
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = topBarHeight + 6.dp)
|
||||
.padding(top = topBarHeight)
|
||||
.padding(horizontal = 14.dp)
|
||||
) {
|
||||
if (hasBridgeError) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -27,11 +28,15 @@ import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
@@ -62,7 +67,8 @@ class SocialRootSection : Routes.Route() {
|
||||
private fun ScopeList(
|
||||
scope: SocialScope,
|
||||
friends: List<MessagingFriendInfo>,
|
||||
groups: List<MessagingGroupInfo>
|
||||
groups: List<MessagingGroupInfo>,
|
||||
controlsHeight: androidx.compose.ui.unit.Dp
|
||||
) {
|
||||
val remainingHours = remember { context.config.root.streaksReminder.remainingHours.get() }
|
||||
val list = when (scope) {
|
||||
@@ -70,10 +76,17 @@ class SocialRootSection : Routes.Route() {
|
||||
SocialScope.FRIEND -> friends
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect(listState.firstVisibleItemScrollOffset, listState.firstVisibleItemIndex) {
|
||||
val offset = if (listState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else listState.firstVisibleItemScrollOffset
|
||||
routes.navigation?.globalScrollOffset = offset
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 10.dp, end = 10.dp, bottom = routes.bottomPadding + 12.dp),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(start = 10.dp, end = 10.dp, top = controlsHeight, bottom = routes.bottomPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
//check if scope list is empty
|
||||
@@ -194,6 +207,8 @@ class SocialRootSection : Routes.Route() {
|
||||
val pagerState = rememberPagerState { titles.size }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
updateScopeLists()
|
||||
@@ -234,10 +249,11 @@ class SocialRootSection : Routes.Route() {
|
||||
onSearchToggle = {
|
||||
searchActive = !searchActive
|
||||
if (!searchActive) searchQuery = ""
|
||||
}
|
||||
},
|
||||
onPositioned = { controlsHeight = it }
|
||||
)
|
||||
if (searchActive) {
|
||||
val searchHint = context.translation["manager.dialogs.add_friend.search_hint"]
|
||||
val searchHint = context.translation["manager.dialogs.add_friend.search_hint"] ?: "Search"
|
||||
val searchShape = RoundedCornerShape(18.dp)
|
||||
val searchBorder = Brush.linearGradient(
|
||||
listOf(
|
||||
@@ -307,8 +323,8 @@ class SocialRootSection : Routes.Route() {
|
||||
state = pagerState
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> ScopeList(SocialScope.FRIEND, filteredFriends, filteredGroups)
|
||||
1 -> ScopeList(SocialScope.GROUP, filteredFriends, filteredGroups)
|
||||
0 -> ScopeList(SocialScope.FRIEND, filteredFriends, filteredGroups, controlsHeight = controlsHeight)
|
||||
1 -> ScopeList(SocialScope.GROUP, filteredFriends, filteredGroups, controlsHeight = controlsHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,7 +391,7 @@ class SocialRootSection : Routes.Route() {
|
||||
fontSize = 15.sp
|
||||
)
|
||||
Text(
|
||||
text = translation["groups_tab"],
|
||||
text = translation["groups_tab"] ?: "Groups",
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
@@ -485,68 +501,50 @@ class SocialRootSection : Routes.Route() {
|
||||
friendCount: Int,
|
||||
groupCount: Int,
|
||||
searchActive: Boolean,
|
||||
onSearchToggle: () -> Unit
|
||||
onSearchToggle: () -> Unit,
|
||||
onPositioned: (Dp) -> Unit = {}
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
color = Color.White.copy(alpha = 0.07f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
val scrollOffset = routes.navigation?.globalScrollOffset ?: 0
|
||||
val focusFactor = (scrollOffset / Motion.HEADER_MORPH_THRESHOLD).coerceIn(0f, 1f)
|
||||
val tabSwitcherAlpha = (1f - (focusFactor * 2.5f)).coerceIn(0f, 1f)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.headerHeightTracker(onPositioned),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = translation["manager.routes.social"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 18.sp
|
||||
me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar(
|
||||
title = translation["manager.routes.social"] ?: "Social",
|
||||
subtitle = if (pagerState.currentPage == 0) translation["friends_tab"] else translation["groups_tab"],
|
||||
scrollOffset = scrollOffset,
|
||||
actions = {
|
||||
StatPill(label = translation["friends_tab"], value = friendCount)
|
||||
StatPill(label = translation["groups_tab"], value = groupCount)
|
||||
IconButton(onClick = onSearchToggle) {
|
||||
Icon(
|
||||
imageVector = if (searchActive) Icons.Filled.Close else Icons.Filled.Search,
|
||||
contentDescription = if (searchActive) translation["close_search_button_description"] else translation["search_button_description"],
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StatPill(label = translation["friends_tab"], value = friendCount)
|
||||
StatPill(label = translation["groups_tab"], value = groupCount)
|
||||
IconButton(onClick = onSearchToggle) {
|
||||
Icon(
|
||||
imageVector = if (searchActive) Icons.Filled.Close else Icons.Filled.Search,
|
||||
contentDescription = if (searchActive) translation["close_search_button_description"] else translation["search_button_description"],
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SocialTabSwitcher(
|
||||
titles = titles,
|
||||
pagerState = pagerState,
|
||||
onTabSelected = onTabSelected
|
||||
)
|
||||
)
|
||||
|
||||
if (tabSwitcherAlpha > 0.05f) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp)
|
||||
.graphicsLayer {
|
||||
alpha = tabSwitcherAlpha
|
||||
translationY = (-10 * focusFactor).dp.toPx()
|
||||
}
|
||||
) {
|
||||
SocialTabSwitcher(
|
||||
titles = titles,
|
||||
pagerState = pagerState,
|
||||
onTabSelected = onTabSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -631,7 +629,7 @@ class SocialRootSection : Routes.Route() {
|
||||
fontSize = 15.sp
|
||||
)
|
||||
Text(
|
||||
text = translation["social_empty_hint"],
|
||||
text = translation["social_empty_hint"] ?: "Tap the + button to sync friends or groups.",
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
|
||||
@@ -496,7 +496,7 @@ class EditRule : Routes.Route() {
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
.padding(padding)
|
||||
) {
|
||||
val contentBottomPadding = routes.bottomPadding + 12.dp
|
||||
val contentBottomPadding = routes.bottomPadding
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
@@ -32,7 +33,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -50,6 +50,8 @@ import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticEmptyState
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
@@ -69,7 +71,10 @@ class FriendTrackerCatalog : Routes.Route() {
|
||||
override val translation by lazy { context.translation.getCategory("manager.friend_tracker_catalog") }
|
||||
|
||||
@Composable
|
||||
private fun AvailableRulesTab(topBarHeight: Dp) {
|
||||
private fun AvailableRulesTab(
|
||||
controlsHeight: androidx.compose.ui.unit.Dp,
|
||||
onScrollOffsetChanged: (Int) -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val okHttpClient = remember { OkHttpClient() }
|
||||
val gson = remember { context.gson }
|
||||
@@ -78,7 +83,6 @@ class FriendTrackerCatalog : Routes.Route() {
|
||||
var repoIndexes by remember { mutableStateOf<Map<String, FriendTrackerRepoManifest>>(emptyMap()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
// Ticks whenever a rule import happens so isImported values recompute
|
||||
var importTick by remember { mutableStateOf(0) }
|
||||
|
||||
fun refreshIndexes() {
|
||||
@@ -93,7 +97,7 @@ class FriendTrackerCatalog : Routes.Route() {
|
||||
repos.forEach { repoRoot ->
|
||||
val indexUrl = if (repoRoot.endsWith("/")) "${repoRoot}index.json" else "$repoRoot/index.json"
|
||||
try {
|
||||
val req = Request.Builder().url(indexUrl).build() // ktlint-disable indent_wrapped_argument
|
||||
val req = Request.Builder().url(indexUrl).build()
|
||||
okHttpClient.newCall(req).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
response.body?.charStream()?.let { reader ->
|
||||
@@ -160,14 +164,14 @@ class FriendTrackerCatalog : Routes.Route() {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val rawUrl = if (repoUrl.endsWith("/")) repoUrl + entry.path else repoUrl + "/" + entry.path
|
||||
try {
|
||||
val req = Request.Builder().url(rawUrl).build() // ktlint-disable indent_wrapped_argument
|
||||
val req = Request.Builder().url(rawUrl).build()
|
||||
okHttpClient.newCall(req).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
withContext(Dispatchers.Main) { context.shortToast(translation.format("download_failed", "code" to response.code.toString())) }
|
||||
return@use
|
||||
}
|
||||
val content = response.body?.string()
|
||||
if (content != null) { // ktlint-disable no-multi-spaces
|
||||
if (content != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
routes.friendTrackerConfigJsonForImport = content
|
||||
routes.friendTrackerConfigImport.navigate()
|
||||
@@ -182,27 +186,21 @@ class FriendTrackerCatalog : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
if (repositories.isEmpty() && !isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = translation["no_repos_added"],
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemScrollOffset, listState.firstVisibleItemIndex) {
|
||||
val offset = if (listState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else listState.firstVisibleItemScrollOffset
|
||||
onScrollOffsetChanged(offset)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(
|
||||
start = 8.dp,
|
||||
top = topBarHeight + 12.dp,
|
||||
top = controlsHeight,
|
||||
end = 8.dp,
|
||||
bottom = 8.dp + routes.bottomPadding
|
||||
bottom = routes.bottomPadding
|
||||
)
|
||||
) {
|
||||
item {
|
||||
@@ -233,7 +231,6 @@ class FriendTrackerCatalog : Routes.Route() {
|
||||
}
|
||||
}
|
||||
items(allRules) { (repoUrl, entry) ->
|
||||
// Compute isImported using produceState so the suspend db call runs in a coroutine
|
||||
val isImported by produceState(initialValue = false, key1 = entry.name, key2 = importTick) {
|
||||
val exists = withContext(Dispatchers.IO) {
|
||||
context.database.getTrackerRuleByName(entry.name) != null
|
||||
@@ -327,35 +324,34 @@ class FriendTrackerCatalog : Routes.Route() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val content: @Composable (NavBackStackEntry) -> Unit = {
|
||||
val density = LocalDensity.current
|
||||
val statusBarTopPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
var topBarHeight by remember { mutableStateOf(statusBarTopPadding + 96.dp) }
|
||||
var scrollOffset by remember { mutableIntStateOf(0) }
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
AvailableRulesTab(
|
||||
controlsHeight = controlsHeight,
|
||||
onScrollOffsetChanged = { scrollOffset = it }
|
||||
)
|
||||
|
||||
FloatingTopBar(
|
||||
title = translation["title"],
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = scrollOffset,
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||
actions = {
|
||||
IconButton(onClick = { routes.manageFriendTrackerRepos.navigate() }) {
|
||||
Icon(Icons.Default.Public, contentDescription = translation["manage_repos_description"], tint = Color.White)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.zIndex(2f)
|
||||
.onGloballyPositioned {
|
||||
val newHeight = with(density) { it.size.height.toDp() }
|
||||
if (newHeight != topBarHeight) topBarHeight = newHeight
|
||||
}
|
||||
}
|
||||
)
|
||||
AvailableRulesTab(topBarHeight = topBarHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.AutoGraph
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
@@ -25,13 +25,7 @@ import androidx.compose.material.icons.filled.FileOpen
|
||||
import androidx.compose.material.icons.filled.Store
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -53,12 +47,12 @@ import me.eternal.purrfectsnap.storage.*
|
||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
import me.eternal.purrfectsnap.ui.util.ActivityLauncherHelper
|
||||
import me.eternal.purrfectsnap.ui.util.coil.BitmojiImage
|
||||
import me.eternal.purrfectsnap.ui.util.openFile
|
||||
import me.eternal.purrfectsnap.ui.util.purrfectSwitchColors
|
||||
import me.eternal.purrfectsnap.ui.util.pagerTabIndicatorOffset
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
class FriendTrackerManagerRoot : Routes.Route() {
|
||||
@@ -160,8 +154,13 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
label: String,
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
scrollOffset: Int = 0
|
||||
) {
|
||||
val shrinkThreshold = 300f
|
||||
val focusFactor = (scrollOffset / shrinkThreshold).coerceIn(0f, 1f)
|
||||
val labelAlpha = (1f - (focusFactor * 2.5f)).coerceIn(0f, 1f)
|
||||
|
||||
val shape = RoundedCornerShape(18.dp)
|
||||
val backgroundBrush = remember {
|
||||
Brush.linearGradient(
|
||||
@@ -185,123 +184,25 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
.background(backgroundBrush, shape)
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(if (labelAlpha > 0.05f) 8.dp else 0.dp)
|
||||
) {
|
||||
Icon(icon, contentDescription = label, tint = Color.White)
|
||||
Text(label, color = Color.White, fontWeight = FontWeight.SemiBold)
|
||||
Icon(icon, contentDescription = label, tint = Color.White, modifier = Modifier.size(20.dp))
|
||||
if (labelAlpha > 0.05f) {
|
||||
Text(
|
||||
label,
|
||||
color = Color.White.copy(alpha = labelAlpha),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val topBarActions: @Composable RowScope.() -> Unit = {
|
||||
var showExportDialog by remember { mutableStateOf(false) }
|
||||
var showSingleExportDialog by remember { mutableStateOf(false) }
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
var showInvalidImportTypeDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showExportDialog) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showExportDialog = false },
|
||||
title = translation["export_dialog_title"],
|
||||
text = translation["export_logs_dialog_confirm_text"],
|
||||
icon = Icons.Default.SaveAlt,
|
||||
confirmButtonText = translation["export_button"],
|
||||
onConfirm = {
|
||||
showExportDialog = false
|
||||
routes.friendTrackerConfigExport.navigate()
|
||||
},
|
||||
dismissButtonText = translation["button.cancel"],
|
||||
onDismiss = { showExportDialog = false },
|
||||
opaque = true,
|
||||
showCloseButton = false
|
||||
)
|
||||
}
|
||||
|
||||
if (showSingleExportDialog) {
|
||||
val rules = rememberAsyncMutableStateList(defaultValue = emptyList()) {
|
||||
context.database.getTrackerRulesDesc()
|
||||
}
|
||||
SelectRuleDialog(
|
||||
onDismissRequest = { showSingleExportDialog = false },
|
||||
rules = rules,
|
||||
onRuleSelected = { rule ->
|
||||
showSingleExportDialog = false
|
||||
routes.friendTrackerConfigExport.navigate {
|
||||
this["rule_id"] = rule.id.toString()
|
||||
}
|
||||
},
|
||||
translation = translation
|
||||
)
|
||||
}
|
||||
|
||||
fun handleImport(type: me.eternal.purrfectsnap.common.data.ExportType) {
|
||||
routes.activityLauncher.openFile("application/json") { uri ->
|
||||
runCatching {
|
||||
val content = context.androidContext.contentResolver.openInputStream(android.net.Uri.parse(uri))?.use {
|
||||
it.readBytes().toString(Charsets.UTF_8)
|
||||
} ?: return@runCatching
|
||||
val exportedData = context.gson.fromJson(content, me.eternal.purrfectsnap.common.data.ExportedTrackerData::class.java)
|
||||
if (exportedData.type != type) {
|
||||
showInvalidImportTypeDialog = true
|
||||
return@runCatching
|
||||
}
|
||||
routes.friendTrackerConfigJsonForImport = content
|
||||
routes.friendTrackerConfigImport.navigate()
|
||||
}.onFailure {
|
||||
context.longToast(
|
||||
translation.format("read_file_failed_toast", "message" to (it.message ?: ""))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showInvalidImportTypeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showInvalidImportTypeDialog = false },
|
||||
title = { Text(translation["invalid_import_type_dialog_title"]) },
|
||||
text = { Text(translation["invalid_import_type_dialog_text"]) },
|
||||
confirmButton = {
|
||||
Button(onClick = { showInvalidImportTypeDialog = false }) {
|
||||
Text(translation["button.ok"])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showImportDialog) {
|
||||
AestheticDialog(
|
||||
onDismissRequest = { showImportDialog = false },
|
||||
title = translation["import_dialog_title"],
|
||||
text = translation["import_dialog_subtitle"] ?: translation["import_dialog_title"],
|
||||
icon = Icons.Default.FolderOpen,
|
||||
confirmButtonText = translation["bulk_import_button"],
|
||||
onConfirm = {
|
||||
showImportDialog = false
|
||||
handleImport(me.eternal.purrfectsnap.common.data.ExportType.BULK)
|
||||
},
|
||||
dismissButtonText = translation["individual_import_button"],
|
||||
onDismiss = {
|
||||
showImportDialog = false
|
||||
handleImport(me.eternal.purrfectsnap.common.data.ExportType.SINGLE)
|
||||
},
|
||||
opaque = true,
|
||||
showCloseButton = false
|
||||
)
|
||||
}
|
||||
|
||||
if (currentPage == 0) {
|
||||
TrackerIconButton(
|
||||
icon = Icons.Default.FolderOpen,
|
||||
contentDescription = translation["import_button_description"],
|
||||
onClick = { showImportDialog = true }
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
TrackerIconButton(
|
||||
icon = Icons.Default.SaveAlt,
|
||||
contentDescription = translation["export_button_description"],
|
||||
onClick = { showExportDialog = true }
|
||||
)
|
||||
}
|
||||
// Handled via FloatingTopBar
|
||||
}
|
||||
|
||||
private lateinit var activityLauncherHelper: ActivityLauncherHelper
|
||||
@@ -347,11 +248,18 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfigRulesTab() {
|
||||
private fun ConfigRulesTab(scrollOffset: (Int) -> Unit) {
|
||||
val updateRules = rememberAsyncUpdateDispatcher()
|
||||
val rules = rememberAsyncMutableStateList(defaultValue = listOf(), updateDispatcher = updateRules) {
|
||||
context.database.getTrackerRulesDesc()
|
||||
}
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemScrollOffset, listState.firstVisibleItemIndex) {
|
||||
val offset = if (listState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else listState.firstVisibleItemScrollOffset
|
||||
scrollOffset(offset)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyState(text: String) {
|
||||
Column(
|
||||
@@ -389,178 +297,175 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(bottom = routes.bottomPadding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(bottom = routes.bottomPadding)
|
||||
) {
|
||||
item {
|
||||
if (rules.isEmpty()) {
|
||||
EmptyState(translation["no_rules_found"])
|
||||
}
|
||||
item {
|
||||
if (rules.isEmpty()) {
|
||||
EmptyState(translation["no_rules_found"])
|
||||
}
|
||||
}
|
||||
items(rules, key = { it.id }) { rule ->
|
||||
val ruleName by rememberAsyncMutableState(defaultValue = rule.name) {
|
||||
context.database.getTrackerRule(rule.id)?.name ?: translation["empty_rule_name"]
|
||||
}
|
||||
val eventCount by rememberAsyncMutableState(defaultValue = 0) {
|
||||
context.database.getTrackerEvents(rule.id).size
|
||||
}
|
||||
val scopeCount by rememberAsyncMutableState(defaultValue = 0) {
|
||||
context.database.getRuleTrackerScopes(rule.id).size
|
||||
}
|
||||
var enabled by rememberAsyncMutableState(defaultValue = rule.enabled) {
|
||||
context.database.getTrackerRule(rule.id)?.enabled ?: false
|
||||
}
|
||||
items(rules, key = { it.id }) { rule ->
|
||||
val ruleName by rememberAsyncMutableState(defaultValue = rule.name) {
|
||||
context.database.getTrackerRule(rule.id)?.name ?: translation["empty_rule_name"]
|
||||
}
|
||||
val eventCount by rememberAsyncMutableState(defaultValue = 0) {
|
||||
context.database.getTrackerEvents(rule.id).size
|
||||
}
|
||||
val scopeCount by rememberAsyncMutableState(defaultValue = 0) {
|
||||
context.database.getRuleTrackerScopes(rule.id).size
|
||||
}
|
||||
var enabled by rememberAsyncMutableState(defaultValue = rule.enabled) {
|
||||
context.database.getTrackerRule(rule.id)?.enabled ?: false
|
||||
}
|
||||
|
||||
val ruleShape = RoundedCornerShape(20.dp)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
routes.editRule.navigate {
|
||||
this["rule_id"] = rule.id.toString()
|
||||
}
|
||||
val ruleShape = RoundedCornerShape(20.dp)
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
routes.editRule.navigate {
|
||||
this["rule_id"] = rule.id.toString()
|
||||
}
|
||||
.padding(8.dp),
|
||||
shape = ruleShape,
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 12.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.5f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.4f)
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
shape = ruleShape,
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 12.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.5f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.4f)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(PurrfectPalette.cardOverlay, ruleShape)
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(PurrfectPalette.cardOverlay, ruleShape)
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f))
|
||||
) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.18f))
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(54.dp)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.35f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.3f)
|
||||
)
|
||||
),
|
||||
CircleShape
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(54.dp)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.35f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.3f)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.Rule, contentDescription = null, tint = Color.White)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(ruleName, fontSize = 18.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
|
||||
Text(
|
||||
buildString {
|
||||
append(eventCount)
|
||||
Icon(Icons.AutoMirrored.Filled.Rule, contentDescription = null, tint = Color.White)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(ruleName, fontSize = 18.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
|
||||
Text(
|
||||
buildString {
|
||||
append(eventCount)
|
||||
append(" ")
|
||||
append(translation["events_suffix"])
|
||||
if (scopeCount > 0) {
|
||||
append(" • ")
|
||||
append(scopeCount)
|
||||
append(" ")
|
||||
append(translation["events_suffix"])
|
||||
if (scopeCount > 0) {
|
||||
append(" • ")
|
||||
append(scopeCount)
|
||||
append(" ")
|
||||
append(translation["scopes_suffix"])
|
||||
}
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
if (scopeCount > 0) {
|
||||
val scopesBitmoji = rememberAsyncMutableStateList(defaultValue = emptyList()) {
|
||||
context.database.getRuleTrackerScopes(rule.id, limit = 8).mapNotNull {
|
||||
context.database.getFriendInfo(it.key)?.let { friend ->
|
||||
friend.selfieId to friend.bitmojiId
|
||||
}
|
||||
append(translation["scopes_suffix"])
|
||||
}
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
if (scopeCount > 0) {
|
||||
val scopesBitmoji = rememberAsyncMutableStateList(defaultValue = emptyList()) {
|
||||
context.database.getRuleTrackerScopes(rule.id, limit = 8).mapNotNull {
|
||||
context.database.getFriendInfo(it.key)?.let { friend ->
|
||||
friend.selfieId to friend.bitmojiId
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy((-10).dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
scopesBitmoji.take(4).forEach { friend ->
|
||||
BitmojiImage(
|
||||
size = 34,
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy((-10).dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
scopesBitmoji.take(4).forEach { friend ->
|
||||
BitmojiImage(
|
||||
size = 34,
|
||||
modifier = Modifier
|
||||
.border(BorderStroke(1.dp, Color.White), CircleShape)
|
||||
.background(Color.White, CircleShape)
|
||||
.clip(CircleShape),
|
||||
context = context,
|
||||
url = BitmojiSelfie.getBitmojiSelfie(friend.first, friend.second, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D),
|
||||
)
|
||||
}
|
||||
if (scopeCount > scopesBitmoji.size) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Text(
|
||||
text = "+${scopeCount - scopesBitmoji.size}",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.border(BorderStroke(1.dp, Color.White), CircleShape)
|
||||
.background(Color.White, CircleShape)
|
||||
.clip(CircleShape),
|
||||
context = context,
|
||||
url = BitmojiSelfie.getBitmojiSelfie(friend.first, friend.second, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D),
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
if (scopeCount > scopesBitmoji.size) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Text(
|
||||
text = "+${scopeCount - scopesBitmoji.size}",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
|
||||
) {
|
||||
Text(
|
||||
text = translation[if (enabled) "enabled_label" else "disabled_label"],
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = enabled,
|
||||
onCheckedChange = {
|
||||
enabled = it
|
||||
context.database.setTrackerRuleState(rule.id, it)
|
||||
},
|
||||
colors = purrfectSwitchColors()
|
||||
Text(
|
||||
text = translation[if (enabled) "enabled_label" else "disabled_label"],
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = enabled,
|
||||
onCheckedChange = {
|
||||
enabled = it
|
||||
context.database.setTrackerRuleState(rule.id, it)
|
||||
},
|
||||
colors = purrfectSwitchColors()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,10 +479,14 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val pagerState = rememberPagerState(initialPage = 0) { titles.size }
|
||||
currentPage = pagerState.currentPage
|
||||
|
||||
var scrollOffset by remember { mutableIntStateOf(0) }
|
||||
var showExportDialog by remember { mutableStateOf(false) }
|
||||
var showSingleExportDialog by remember { mutableStateOf(false) }
|
||||
var showImportDialog by remember { mutableStateOf(false) }
|
||||
var showInvalidImportTypeDialog by remember { mutableStateOf(false) }
|
||||
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||
var controlsHeight by remember { mutableStateOf(100.dp) }
|
||||
|
||||
fun handleImport(type: me.eternal.purrfectsnap.common.data.ExportType) {
|
||||
routes.activityLauncher.openFile("application/json") { uri ->
|
||||
@@ -606,125 +515,77 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
.background(PurrfectPalette.backgroundGradient)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Surface(
|
||||
me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar(
|
||||
title = context.translation["manager.routes.friend_tracker"],
|
||||
subtitle = titles.getOrNull(pagerState.currentPage) ?: "",
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
scrollOffset = scrollOffset,
|
||||
modifier = Modifier.headerHeightTracker { controlsHeight = it },
|
||||
actions = {
|
||||
if (pagerState.currentPage == 0) {
|
||||
TrackerPillButton(
|
||||
label = translation["import_button"],
|
||||
icon = Icons.Default.FolderOpen,
|
||||
scrollOffset = scrollOffset,
|
||||
onClick = { showImportDialog = true }
|
||||
)
|
||||
TrackerPillButton(
|
||||
label = translation["export_button"],
|
||||
icon = Icons.Default.SaveAlt,
|
||||
scrollOffset = scrollOffset,
|
||||
onClick = { showExportDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()),
|
||||
shape = RoundedCornerShape(26.dp),
|
||||
color = PurrfectPalette.cardOverlayColor,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 10.dp,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
PurrfectPalette.glowPrimary.copy(alpha = 0.55f),
|
||||
PurrfectPalette.glowSecondary.copy(alpha = 0.35f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
titles.forEachIndexed { i, text ->
|
||||
val selected = pagerState.currentPage == i
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f).clip(RoundedCornerShape(18.dp)).clickable {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(i) }
|
||||
},
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (selected) Color.White.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.06f),
|
||||
border = if (selected) BorderStroke(1.dp, Brush.linearGradient(listOf(PurrfectPalette.glowPrimary, PurrfectPalette.glowSecondary))) else BorderStroke(1.dp, Color.White.copy(alpha = 0.16f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
text = context.translation["manager.routes.friend_tracker"],
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Text(
|
||||
text = titles.getOrNull(pagerState.currentPage) ?: "",
|
||||
color = PurrfectPalette.textSecondary,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
if (pagerState.currentPage == 0) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
TrackerPillButton(
|
||||
label = translation["import_button"],
|
||||
icon = Icons.Default.FolderOpen,
|
||||
onClick = { showImportDialog = true }
|
||||
)
|
||||
TrackerPillButton(
|
||||
label = translation["export_button"],
|
||||
icon = Icons.Default.SaveAlt,
|
||||
onClick = { showExportDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(text = text, color = Color.White, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Surface(
|
||||
HorizontalPager(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
color = Color.White.copy(alpha = 0.04f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.08f))
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
titles.forEachIndexed { i, text ->
|
||||
val selected = pagerState.currentPage == i
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f).clip(RoundedCornerShape(18.dp)).clickable {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(i) }
|
||||
},
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (selected) Color.White.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.06f),
|
||||
border = if (selected) BorderStroke(1.dp, Brush.linearGradient(listOf(PurrfectPalette.glowPrimary, PurrfectPalette.glowSecondary))) else BorderStroke(1.dp, Color.White.copy(alpha = 0.16f)),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(text = text, color = Color.White, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp),
|
||||
state = pagerState
|
||||
) { page ->
|
||||
when (page) {
|
||||
1 -> LogsTab(
|
||||
context = context,
|
||||
activityLauncherHelper = activityLauncherHelper,
|
||||
deleteAction = { logDeleteAction = it },
|
||||
exportAction = { exportAction = it },
|
||||
bottomPadding = routes.bottomPadding
|
||||
)
|
||||
0 -> ConfigRulesTab()
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp),
|
||||
state = pagerState
|
||||
) { page ->
|
||||
when (page) {
|
||||
1 -> LogsTab(
|
||||
context = context,
|
||||
activityLauncherHelper = activityLauncherHelper,
|
||||
deleteAction = { logDeleteAction = it },
|
||||
exportAction = { exportAction = it },
|
||||
bottomPadding = routes.bottomPadding,
|
||||
scrollOffset = { scrollOffset = it }
|
||||
)
|
||||
0 -> ConfigRulesTab(scrollOffset = { scrollOffset = it })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,8 +596,8 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
onDismissRequest = { showExportDialog = false },
|
||||
title = translation["export_dialog_title"],
|
||||
choices = listOf(
|
||||
translation["bulk_export_button"] to { Icon(Icons.Default.UploadFile, translation["bulk_export_button"]) },
|
||||
translation["individual_export_button"] to { Icon(Icons.Default.FileOpen, translation["individual_export_button"]) }
|
||||
translation["bulk_export_button"] to { Icon(Icons.Default.UploadFile, translation["bulk_export_button"], tint = Color.White) },
|
||||
translation["individual_export_button"] to { Icon(Icons.Default.FileOpen, translation["individual_export_button"], tint = Color.White) }
|
||||
),
|
||||
onChoiceSelected = { index ->
|
||||
showExportDialog = false
|
||||
@@ -783,8 +644,8 @@ class FriendTrackerManagerRoot : Routes.Route() {
|
||||
onDismissRequest = { showImportDialog = false },
|
||||
title = translation["import_dialog_title"],
|
||||
choices = listOf(
|
||||
translation["bulk_import_button"] to { Icon(Icons.Default.UploadFile, translation["bulk_import_button"]) },
|
||||
translation["individual_import_button"] to { Icon(Icons.Default.FileOpen, translation["individual_import_button"]) }
|
||||
translation["bulk_import_button"] to { Icon(Icons.Default.UploadFile, translation["bulk_import_button"], tint = Color.White) },
|
||||
translation["individual_import_button"] to { Icon(Icons.Default.FileOpen, translation["individual_import_button"], tint = Color.White) }
|
||||
),
|
||||
onChoiceSelected = { index ->
|
||||
showImportDialog = false
|
||||
@@ -806,33 +667,51 @@ private fun SelectRuleDialog(
|
||||
translation: me.eternal.purrfectsnap.common.bridge.wrapper.LocaleWrapper
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
Surface(
|
||||
shape = shape,
|
||||
color = Color.Transparent,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 20.dp,
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, shape)
|
||||
.padding(18.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(translation["manager.friend_tracker.select_rule_to_export_title"], style = MaterialTheme.typography.headlineSmall)
|
||||
Text(
|
||||
translation["manager.friend_tracker.select_rule_to_export_title"],
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.heightIn(max = 300.dp)
|
||||
) {
|
||||
items(rules) { rule ->
|
||||
ElevatedCard(
|
||||
Surface(
|
||||
onClick = { onRuleSelected(rule) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.1f))
|
||||
) {
|
||||
Text(
|
||||
text = rule.name,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(translation["button.cancel"])
|
||||
Text(translation["button.cancel"], color = PurrfectPalette.glowSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,7 +726,7 @@ private fun ChoiceDialog(
|
||||
onChoiceSelected: (Int) -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
val shape = RoundedCornerShape(20.dp)
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
Surface(
|
||||
shape = shape,
|
||||
color = Color.Transparent,
|
||||
@@ -865,9 +744,9 @@ private fun ChoiceDialog(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PurrfectPalette.cardOverlay, shape)
|
||||
.padding(horizontal = 18.dp, vertical = 16.dp),
|
||||
.padding(horizontal = 20.dp, vertical = 22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
@@ -876,36 +755,24 @@ private fun ChoiceDialog(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
choices.forEachIndexed { index, (text, icon) ->
|
||||
SelectButton(
|
||||
Surface(
|
||||
onClick = { onChoiceSelected(index) },
|
||||
text = text,
|
||||
leadingIcon = icon
|
||||
)
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = Color.White.copy(alpha = 0.08f),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.12f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
icon()
|
||||
Text(text = text, modifier = Modifier.weight(1f), color = Color.White, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectButton(
|
||||
onClick: () -> Unit,
|
||||
text: String,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
if (leadingIcon != null) {
|
||||
leadingIcon()
|
||||
}
|
||||
Text(text = text, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package me.eternal.purrfectsnap.ui.manager.pages.tracker
|
||||
|
||||
import me.eternal.purrfectsnap.ui.util.Motion
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
@@ -7,6 +9,7 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -61,6 +64,7 @@ fun LogsTab(
|
||||
deleteAction: (() -> Unit) -> Unit,
|
||||
exportAction: (() -> Unit) -> Unit,
|
||||
bottomPadding: Dp,
|
||||
scrollOffset: (Int) -> Unit
|
||||
) {
|
||||
val translation = remember { context.translation.getCategory("manager.friend_tracker") }
|
||||
val trackerTranslation = remember { context.translation.getCategory("tracker") }
|
||||
@@ -78,6 +82,12 @@ fun LogsTab(
|
||||
var filter by remember { mutableStateOf("") }
|
||||
var searchTimeoutJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect(listState.firstVisibleItemScrollOffset, listState.firstVisibleItemIndex) {
|
||||
val offset = if (listState.firstVisibleItemIndex > 0) Motion.HEADER_MORPH_THRESHOLD.toInt() else listState.firstVisibleItemScrollOffset
|
||||
scrollOffset(offset)
|
||||
}
|
||||
|
||||
fun getPaginatedLogs(pageIndex: Int) = context.messageLogger.getLogs(
|
||||
pageIndex = pageIndex,
|
||||
pageSize = 30,
|
||||
@@ -665,13 +675,15 @@ fun LogsTab(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(bottom = bottomPadding)
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(bottom = bottomPadding)
|
||||
) {
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -73,6 +73,7 @@ import me.eternal.purrfectsnap.ui.manager.components.AestheticDialog
|
||||
import me.eternal.purrfectsnap.ui.manager.components.AestheticEmptyState
|
||||
import me.eternal.purrfectsnap.ui.manager.components.FloatingTopBar
|
||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
||||
import me.eternal.purrfectsnap.ui.util.headerHeightTracker
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class ManageFriendTrackerReposSection: Routes.Route() {
|
||||
@@ -286,10 +287,7 @@ class ManageFriendTrackerReposSection: Routes.Route() {
|
||||
onBack = { routes.navController.popBackStack() },
|
||||
modifier = Modifier
|
||||
.zIndex(2f)
|
||||
.onGloballyPositioned {
|
||||
val newHeight = with(density) { it.size.height.toDp() }
|
||||
if (newHeight != topBarHeight) topBarHeight = newHeight
|
||||
}
|
||||
.headerHeightTracker { topBarHeight = it }
|
||||
)
|
||||
if (repositories.isEmpty()) {
|
||||
Box(
|
||||
@@ -309,9 +307,9 @@ class ManageFriendTrackerReposSection: Routes.Route() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 12.dp,
|
||||
top = topBarHeight + 12.dp,
|
||||
top = topBarHeight,
|
||||
end = 12.dp,
|
||||
bottom = 18.dp + routes.bottomPadding
|
||||
bottom = routes.bottomPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
|
||||
@@ -68,7 +68,7 @@ class RemoteOverlay(
|
||||
containerColor = Color.Transparent,
|
||||
topBar = { navigation.TopBar() }
|
||||
) { innerPadding ->
|
||||
navigation.NavContent(
|
||||
navigation.Content(
|
||||
innerPadding,
|
||||
startDestination = remember { startRoute(navigation.routes).routeInfo.id }
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package me.eternal.purrfectsnap.ui.setup.screens.impl
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -104,7 +105,14 @@ class SaveFolderScreen : SetupScreen() {
|
||||
color = PurrfectPalette.textSecondary
|
||||
)
|
||||
Text(
|
||||
text = if (currentFolder.isBlank()) context.translation["setup.save_folder.system_default_label"] else currentFolder,
|
||||
text = if (currentFolder.isBlank()) {
|
||||
context.translation["setup.save_folder.system_default_label"]
|
||||
} else {
|
||||
// Decode and simplify the URI for professional display
|
||||
runCatching {
|
||||
Uri.decode(currentFolder).substringAfterLast("%3A", currentFolder).substringAfterLast(":")
|
||||
}.getOrDefault(currentFolder)
|
||||
},
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
|
||||
Reference in New Issue
Block a user