$BUILD_TYPE\n🆔 Build: #$GIT_HASH\n👤 Author: $AUTHOR\n━━━━━━━━━━━━━━━━\n📝 Latest Commits:\n$CHANGELOG\n━━━━━━━━━━━━━━━━\n📂 Assets & Verification:"
-
- # Discover APKs, calculate sizes/hashes, and handle mirrors
- for apk in $(find all-apks -name "*.apk" -type f); do
- FILENAME=$(basename "$apk")
- FILESIZE_BYTES=$(stat -c%s "$apk")
- FILESIZE_MB=$(echo "scale=1; $FILESIZE_BYTES / 1048576" | bc)
- SHA256_FULL=$(sha256sum "$apk" | awk '{ print $1 }')
- SHA256_SHORT=${SHA256_FULL:0:12}
-
- if [[ "$FILENAME" == *"armv8"* || "$FILENAME" == *"arm64"* ]]; then ARCH="ARMv8"; elif [[ "$FILENAME" == *"armv7"* ]]; then ARCH="ARMv7"; else ARCH="Universal"; fi
-
- # 1. Mirror: GoFile
- SERVER=$(curl -s https://api.gofile.io/servers | jq -r '.data.servers[0].name')
- MIRROR_URL=""
- if [ "$SERVER" != "null" ] && [ -n "$SERVER" ]; then
- UPLOAD_RESP=$(curl -s -F "file=@$apk" "https://${SERVER}.gofile.io/uploadFile")
- MIRROR_URL=$(echo "$UPLOAD_RESP" | jq -r '.data.downloadPage')
- fi
-
- # 2. Mirror Fallback: Catbox
- if [ -z "$MIRROR_URL" ] || [ "$MIRROR_URL" == "null" ]; then
- MIRROR_URL=$(curl -s -F "reqtype=fileupload" -F "fileToUpload=@$apk" https://catbox.moe/user/api.php | tr -d '\r\n')
- fi
-
- MESSAGE="$MESSAGE\n📦 $ARCH ($FILESIZE_MB MB): Download\nSHA256: $SHA256_SHORT"
-
- # Direct Telegram Upload (if size < 50MB)
- if [ "$FILESIZE_BYTES" -lt 50000000 ]; then
- curl -s -F "chat_id=$TELEGRAM_CHAT_ID" -F "document=@$apk" -F "caption=📦 $ARCH Build (#$GIT_HASH)" -F "parse_mode=HTML" "https://api.telegram.org/bot$TELEGRAM_TOKEN/sendDocument" > /dev/null
- fi
- done
-
- MESSAGE="$MESSAGE\n━━━━━━━━━━━━━━━━\n🛠 View GitHub Action"
-
- # Final Dispatch
- curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage" -d "chat_id=$TELEGRAM_CHAT_ID" --data-urlencode "text=$(echo -e "$MESSAGE")" -d "parse_mode=HTML" -d "disable_web_page_preview=true"
diff --git a/.gitignore b/.gitignore
index a234d94e..769477b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,3 @@ cloudflare/**/allowed_codes.local.*
security/allowed-codes.local.*
security/allowed_codes.local.*
valdi/node_modules/
-ROADMAP.md
-.github/workflows/upload_telegram.sh
-android9fixes.md
-release.keystore/
diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt
index 9ea9018f..c1ad62ec 100644
--- a/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt
+++ b/app/src/main/kotlin/me/eternal/purrfectsnap/task/AnnouncementCheckWorker.kt
@@ -86,7 +86,7 @@ class AnnouncementCheckWorker(
val pendingIntent = PendingIntent.getActivity(appContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(appContext, channelId)
- .setSmallIcon(R.drawable.launcher_icon_monochrome)
+ .setSmallIcon(R.mipmap.ic_launcher_monochrome)
.setContentTitle(title)
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/task/UpdateCheckWorker.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/task/UpdateCheckWorker.kt
index 0bf9c075..33403a6e 100644
--- a/app/src/main/kotlin/me/eternal/purrfectsnap/task/UpdateCheckWorker.kt
+++ b/app/src/main/kotlin/me/eternal/purrfectsnap/task/UpdateCheckWorker.kt
@@ -65,7 +65,7 @@ class UpdateCheckWorker(
val pendingIntent: PendingIntent = PendingIntent.getActivity(appContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(appContext, channelId)
- .setSmallIcon(R.drawable.launcher_icon_monochrome)
+ .setSmallIcon(R.mipmap.ic_launcher_monochrome)
.setContentTitle(title)
.setContentText(text.format(versionName))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/Navigation.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/Navigation.kt
index d3caf9d0..6175515f 100644
--- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/Navigation.kt
+++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/Navigation.kt
@@ -132,13 +132,14 @@ class Navigation(
val currentRoute = remember(navBackStackEntry) { routes.getCurrentRoute(navBackStackEntry) }
if (currentRoute?.routeInfo?.hasOwnTopBar == true) return
- val shrinkThreshold = 300f
+ val shrinkThreshold = me.eternal.purrfectsnap.ui.util.Motion.HEADER_MORPH_THRESHOLD
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
}
+ val haptic = LocalHapticFeedback.current
TopAppBar(
modifier = Modifier.height(headerHeight),
title = {
@@ -170,7 +171,12 @@ class Navigation(
.width(lerp(0.dp, 48.dp, backButtonAnimation))
.height(48.dp)
) {
- IconButton(onClick = { if (canGoBack) navController.popBackStack() }) {
+ IconButton(onClick = {
+ if (canGoBack) {
+ haptic.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
+ navController.popBackStack()
+ }
+ }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
}
@@ -182,7 +188,10 @@ class Navigation(
actions = {
currentRoute?.topBarActions?.invoke(this)
if (currentRoute?.routeInfo?.id == routes.settings.routeInfo.id) {
- IconButton(onClick = { openBottomBarCustomization = true }) {
+ IconButton(onClick = {
+ haptic.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
+ openBottomBarCustomization = true
+ }) {
Icon(Icons.Filled.Tune, contentDescription = null)
}
}
@@ -191,6 +200,7 @@ class Navigation(
}
@Composable
fun FloatingBottomBar() {
+ val haptic = LocalHapticFeedback.current
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = remember(navBackStackEntry) { routes.getCurrentRoute(navBackStackEntry) }
val availableRoutes = remember {
@@ -198,7 +208,7 @@ class Navigation(
}
val availableRouteMap = remember(availableRoutes) { availableRoutes.associateBy { it.routeInfo.id } }
- val shrinkThreshold = 300f
+ val shrinkThreshold = me.eternal.purrfectsnap.ui.util.Motion.HEADER_MORPH_THRESHOLD
val focusFactor = (globalScrollOffset / shrinkThreshold).coerceIn(0f, 1f)
val barHeight = lerp(82.dp, 64.dp, focusFactor)
val labelAlpha = (1f - (focusFactor * 2.5f)).coerceIn(0f, 1f)
@@ -260,7 +270,7 @@ class Navigation(
val animatedBarWidth by animateDpAsState(targetValue = targetBarWidth ?: 0.dp, label = "barWidth")
Surface(
shape = barShape,
- color = Color.White.copy(alpha = 0.08f), // Restored frosted glass harmony
+ color = Color.White.copy(alpha = 0.08f), // Apply translucent overlay for depth
contentColor = MaterialTheme.colorScheme.onSurface,
border = BorderStroke(
1.dp,
@@ -499,7 +509,10 @@ class Navigation(
unselectedTextColor = Color.White.copy(alpha = 0.72f),
indicatorColor = Color.Transparent
),
- onClick = { route.navigateReset() }
+ onClick = {
+ haptic.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
+ route.navigateReset()
+ }
)
}
}
@@ -848,3 +861,4 @@ class Navigation(
@Composable fun FloatingActionButton() = Fab()
@Composable fun Content(paddingValues: PaddingValues, startDestination: String) = NavContent(paddingValues, startDestination)
}
+
diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt
index 950e2095..4efea7cd 100644
--- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt
+++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/components/FloatingTopBar.kt
@@ -74,65 +74,63 @@ fun FloatingTopBar(
colors: FloatingTopBarColors = rememberDefaultFloatingTopBarColors()
) {
val haptic = LocalHapticFeedback.current
+ val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
- // Use standardized morph threshold
- val focusFactor = (scrollOffset.toFloat() / Motion.HEADER_MORPH_THRESHOLD).coerceIn(0f, 1f)
+ // Calculate morphing factor based on scroll progress
+ val focusFactor by remember(scrollOffset) {
+ derivedStateOf { (scrollOffset.toFloat() / Motion.HEADER_MORPH_THRESHOLD).coerceIn(0f, 1f) }
+ }
- // Haptic "Snap" when header hits full expansion/stickiness
+ val morphingParams by remember(focusFactor, statusBarHeight) {
+ derivedStateOf {
+ object {
+ val headerHeight = lerp(64.dp, 56.dp, focusFactor)
+ val sidePadding = lerp(14.dp, 0.dp, focusFactor)
+ val containerTopPadding = lerp(statusBarHeight + 4.dp, 0.dp, focusFactor)
+ val internalTopPadding = lerp(0.dp, statusBarHeight, focusFactor)
+ val internalVerticalPadding = lerp(8.dp, 0.dp, focusFactor)
+ val topCorners = lerp(26.dp, 0.dp, focusFactor)
+ val bottomCorners = lerp(26.dp, 28.dp, focusFactor)
+ val subtitleAlpha = (1f - (focusFactor * 2.5f)).coerceIn(0f, 1f)
+ val subtitleTranslationY = lerp(0.dp, (-10).dp, focusFactor)
+ val iconScale = 1f - (0.12f * focusFactor)
+ val horizontalShift = (6 * focusFactor).dp
+ }
+ }
+ }
+
+ // Trigger tactile feedback when header reaches full expansion
var hasSnapped by remember { mutableStateOf(false) }
LaunchedEffect(focusFactor) {
if (focusFactor >= 1f && !hasSnapped) {
- haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
hasSnapped = true
} else if (focusFactor < 0.9f) {
hasSnapped = false
}
}
- val statusBarHeight = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
-
- // --- GEOMETRIC MORPHING MATH ---
- // 1. Height: 64dp content area when floating -> 56dp when sticky
- val headerHeight = lerp(64.dp, 56.dp, focusFactor)
-
- // 2. Padding Morph:
- // Static: Island (14dp sides, sits BELOW status bar with 12dp margin)
- // Infinity: Full (0dp sides, covers status bar area entirely)
- val sidePadding = lerp(14.dp, 0.dp, focusFactor)
- val containerTopPadding = lerp(statusBarHeight + 4.dp, 0.dp, focusFactor)
- val internalTopPadding = lerp(0.dp, statusBarHeight, focusFactor)
- val internalVerticalPadding = lerp(8.dp, 0.dp, focusFactor)
-
- // 3. Corner Morph: Round pill (26dp all) -> Bottom-rounded sticky bar (28dp bottom)
- val topCorners = lerp(26.dp, 0.dp, focusFactor)
- val bottomCorners = lerp(26.dp, 28.dp, focusFactor)
- val shape = RoundedCornerShape(
- topStart = topCorners,
- topEnd = topCorners,
- bottomStart = bottomCorners,
- bottomEnd = bottomCorners
- )
-
- // 4. Content Animation: Subtitle falls in/out, icons scale and shift
- val subtitleAlpha = (1f - (focusFactor * 2.5f)).coerceIn(0f, 1f)
- val subtitleTranslationY = lerp(0.dp, (-10).dp, focusFactor)
- val iconScale = 1f - (0.12f * focusFactor)
- val horizontalShift = (6 * focusFactor).dp
+ val shape = remember(morphingParams.topCorners, morphingParams.bottomCorners) {
+ RoundedCornerShape(
+ topStart = morphingParams.topCorners,
+ topEnd = morphingParams.topCorners,
+ bottomStart = morphingParams.bottomCorners,
+ bottomEnd = morphingParams.bottomCorners
+ )
+ }
val borderPath = remember { Path() }
val uPath = remember { Path() }
+ val refractiveColor = remember { Color(0xFF241F52) }
Box(modifier = modifier.fillMaxWidth().zIndex(10f)) {
- // --- 1. REFRACTIVE BACKGROUND (UNDER-GLASS) ---
- // Covers full area (Internal Padding + Content Height + Dissolve Tail)
- val totalHeaderArea = internalTopPadding + headerHeight
- val refractiveColor = Color(0xFF241F52) // Aligned with Palette base
+ // --- 1. Refractive background layer ---
Box(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = sidePadding)
- .padding(top = containerTopPadding)
- .height(totalHeaderArea + 32.dp) // 32dp smooth dissolve tail
+ .padding(horizontal = morphingParams.sidePadding)
+ .padding(top = morphingParams.containerTopPadding)
+ .height(morphingParams.internalTopPadding + morphingParams.headerHeight + 32.dp)
.background(
Brush.verticalGradient(
0.0f to refractiveColor.copy(alpha = 0.95f * focusFactor),
@@ -142,12 +140,12 @@ fun FloatingTopBar(
)
)
- // --- 2. MAIN HEADER SURFACE ---
+ // --- 2. Primary header surface ---
Surface(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = sidePadding)
- .padding(top = containerTopPadding)
+ .padding(horizontal = morphingParams.sidePadding)
+ .padding(top = morphingParams.containerTopPadding)
.graphicsLayer {
alpha = containerAlpha
},
@@ -170,8 +168,8 @@ fun FloatingTopBar(
.drawBehind {
val strokeWidth = 1.dp.toPx()
val brush = Brush.linearGradient(listOf(colors.borderStart, colors.borderEnd))
- val tr = topCorners.toPx()
- val br = bottomCorners.toPx()
+ val tr = morphingParams.topCorners.toPx()
+ val br = morphingParams.bottomCorners.toPx()
if (focusFactor > 0.9f) {
uPath.reset()
@@ -201,25 +199,28 @@ fun FloatingTopBar(
}
}
) {
- // --- 3. ROW CONTENT ---
+ // --- 3. Header layout content ---
Row(
modifier = Modifier
.fillMaxWidth()
- .padding(top = internalTopPadding)
- .padding(horizontal = 16.dp, vertical = internalVerticalPadding)
- .height(headerHeight),
+ .padding(top = morphingParams.internalTopPadding)
+ .padding(horizontal = 16.dp, vertical = morphingParams.internalVerticalPadding)
+ .height(morphingParams.headerHeight),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
if (onBack != null) {
IconButton(
- onClick = onBack,
+ onClick = {
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ onBack()
+ },
modifier = Modifier
.size(44.dp)
.graphicsLayer {
- scaleX = iconScale
- scaleY = iconScale
- translationX = -horizontalShift.toPx()
+ scaleX = morphingParams.iconScale
+ scaleY = morphingParams.iconScale
+ translationX = -morphingParams.horizontalShift.toPx()
}
) {
Icon(
@@ -246,10 +247,10 @@ fun FloatingTopBar(
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
)
- if (!subtitle.isNullOrBlank() && subtitleAlpha > 0.01f) {
+ if (!subtitle.isNullOrBlank() && morphingParams.subtitleAlpha > 0.01f) {
PurrfectMarqueeText(
text = subtitle,
- color = PurrfectPalette.textSecondary.copy(alpha = subtitleAlpha),
+ color = PurrfectPalette.textSecondary.copy(alpha = morphingParams.subtitleAlpha),
style = TextStyle(fontSize = 13.sp),
textAlign = TextAlign.Start,
contentAlignment = Alignment.CenterStart,
@@ -257,8 +258,8 @@ fun FloatingTopBar(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer {
- translationY = subtitleTranslationY.toPx()
- alpha = subtitleAlpha
+ translationY = morphingParams.subtitleTranslationY.toPx()
+ alpha = morphingParams.subtitleAlpha
}
)
}
@@ -268,9 +269,9 @@ fun FloatingTopBar(
modifier = Modifier
.wrapContentWidth()
.graphicsLayer {
- scaleX = iconScale
- scaleY = iconScale
- translationX = horizontalShift.toPx()
+ scaleX = morphingParams.iconScale
+ scaleY = morphingParams.iconScale
+ translationX = morphingParams.horizontalShift.toPx()
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
@@ -282,10 +283,3 @@ fun FloatingTopBar(
}
}
}
-
-/**
- * Extension to safely copy color with clamped alpha.
- */
-private fun Color.coerceCopy(alpha: Float): Color {
- return this.copy(alpha = alpha.coerceIn(0f, 1f))
-}
diff --git a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt
index 88f2da12..9669cfc4 100644
--- a/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt
+++ b/app/src/main/kotlin/me/eternal/purrfectsnap/ui/manager/data/Updater.kt
@@ -14,6 +14,7 @@ object Updater {
val versionName: String,
val releaseUrl: String,
val workflowId: Long?,
+ val body: String? = null,
val assetDownloads: Map~PW>G#b6~C~lttN_e4%69w8uG(DAadq{_% YAV$mf+OK0Xj9eC=05F#pIeQu){OumO?H}vvzV~
zhk4jWFfj$*C|4gqgdZu{)s@q?NGHaj!Mz~oXjdFuyo9{cf)u*CD5h#9{>r$AZtvwGp=3I)~lx3@e}UfgHAE1SzFaNB>;H
H21&}V$diOb0mY;1A=b9zO=
zLx=C&;A~-S-C`kn9a=l3DYu7b|Mwd(Q$ge{M#wWWtA-#R;7
zGs>8dwr*Q6CHclaoKTmRkY3TZehyuxR0QVCXb|mE^AgZ>M%`V@8gs#OR`xkFe{eVB
zJ4(2%KY&88l>h-!u(tR+iR+~3a}*9;fTYr)LB?ViNBGHKL@OV`b3~zZ#wphlf-o{H
z8=n&o?Ky)N@qtgWf2+rMb}pMtvC52f#oYYiz7IB+FEotA5B9M{J;eDLg6`}6FX8^#
z=hZoP%TWJ4ljE