Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Turn any website into an APK, with a simple and easy-to-use UI.
## Known issues

- Custom icons don't work
- Offline caching is experimental and may not work correctly on some advanced or dynamic websites

## Technical Overview

Expand Down
56 changes: 19 additions & 37 deletions app/src/main/java/com/appy/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.appy

import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
Expand All @@ -9,11 +10,11 @@ import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
Expand Down Expand Up @@ -122,26 +123,26 @@ class MainActivity : ComponentActivity() {
enterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(200)
) + fadeIn(animationSpec = tween(150))
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(300, easing = FastOutSlowInEasing))
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth / 4 },
animationSpec = tween(200)
) + fadeOut(animationSpec = tween(150))
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(300, easing = FastOutSlowInEasing))
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth / 4 },
animationSpec = tween(200)
) + fadeIn(animationSpec = tween(150))
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(300, easing = FastOutSlowInEasing))
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(200)
) + fadeOut(animationSpec = tween(150))
animationSpec = tween(300, easing = FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(300, easing = FastOutSlowInEasing))
}
) {
composable("home") {
Expand All @@ -156,7 +157,8 @@ class MainActivity : ComponentActivity() {
appName = config.appName,
packageId = config.packageId,
iconUri = config.iconUri,
statusBarDark = config.statusBarStyle == com.appy.ui.screens.StatusBarStyle.DARK
statusBarDark = config.statusBarStyle == com.appy.ui.screens.StatusBarStyle.DARK,
enableOfflineCache = config.enableOfflineCache
).collect { result ->
when (result) {
is ApkProcessingResult.Progress -> {
Expand Down Expand Up @@ -209,45 +211,26 @@ class MainActivity : ComponentActivity() {

/**
* Updates the status bar appearance (icon color) based on the current theme.
* For light themes, uses dark icons. For dark themes, uses light icons.
* Also sets up appropriate scrim colors for predictive back gesture.
* Sets window background drawable for correct predictive-back gesture reveal color,
* and updates system bar icon appearance flags.
*/
@Suppress("DEPRECATION")
private fun updateStatusBarAppearance(isDarkTheme: Boolean) {
// Set window background color to match theme for predictive back gesture scrim
// This affects the "reveal" color during the back gesture animation
if (isDarkTheme) {
window.decorView.setBackgroundColor(Color.parseColor("#1C1B1F")) // Dark background
window.setNavigationBarColor(Color.parseColor("#1C1B1F"))
} else {
window.decorView.setBackgroundColor(Color.parseColor("#FEFBFF")) // Light background
window.setNavigationBarColor(Color.parseColor("#FEFBFF"))
}

// Update edge-to-edge with appropriate system bar styles for the theme
if (isDarkTheme) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)
)
} else {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
)
}
// Set window background for predictive-back gesture reveal color.
// Uses window.setBackgroundDrawable() which is lighter weight than
// decorView.setBackgroundColor() (doesn't trigger full DecorView layout).
val bgColor = if (isDarkTheme) Color.parseColor("#1C1B1F") else Color.parseColor("#FEFBFF")
window.setBackgroundDrawable(ColorDrawable(bgColor))

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.let { controller ->
if (isDarkTheme) {
// Dark theme: clear the light appearance flag (use light/white icons)
controller.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
)
} else {
// Light theme: set light appearance flag (use dark icons)
controller.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
Expand All @@ -257,7 +240,6 @@ class MainActivity : ComponentActivity() {
}
}
} else {
// For older APIs
if (isDarkTheme) {
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/java/com/appy/processor/ApkProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ class ApkProcessor(private val context: Context) {
appName: String = "WebApp",
packageId: String = "com.webapp.app",
iconUri: Uri? = null,
statusBarDark: Boolean = false
statusBarDark: Boolean = false,
enableOfflineCache: Boolean = false
): Flow<ApkProcessingResult> = flow {
try {
emit(ApkProcessingResult.Progress(0.1f, "Preparing template..."))
Expand All @@ -84,7 +85,7 @@ class ApkProcessor(private val context: Context) {
emit(ApkProcessingResult.Progress(0.3f, "Modifying configuration..."))

// Step 3: Modify the APK (inject config.json)
modifyApk(templateFile, outputFile, url, appName, packageId, statusBarDark)
modifyApk(templateFile, outputFile, url, appName, packageId, statusBarDark, enableOfflineCache)
emit(ApkProcessingResult.Progress(0.5f, "Configuration injected"))

// Step 4: Inject custom icon if provided
Expand Down Expand Up @@ -174,7 +175,8 @@ class ApkProcessor(private val context: Context) {
url: String,
appName: String,
packageId: String,
statusBarDark: Boolean
statusBarDark: Boolean,
enableOfflineCache: Boolean
) = withContext(Dispatchers.IO) {
// Copy template to output location
templateFile.copyTo(outputFile, overwrite = true)
Expand All @@ -185,6 +187,7 @@ class ApkProcessor(private val context: Context) {
put("appName", appName)
put("packageId", packageId)
put("statusBarDark", statusBarDark)
put("enableOfflineCache", enableOfflineCache)
put("generatedAt", System.currentTimeMillis())
put("version", "1.0")
}
Expand Down
116 changes: 74 additions & 42 deletions app/src/main/java/com/appy/ui/screens/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
Expand All @@ -68,7 +69,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
Expand Down Expand Up @@ -96,7 +96,8 @@ data class ApkConfig(
val appName: String,
val packageId: String,
val iconUri: Uri?,
val statusBarStyle: StatusBarStyle = StatusBarStyle.LIGHT
val statusBarStyle: StatusBarStyle = StatusBarStyle.LIGHT,
val enableOfflineCache: Boolean = false
)

/**
Expand Down Expand Up @@ -167,6 +168,7 @@ fun HomeScreen(
var iconUri by remember { mutableStateOf<Uri?>(null) }
var statusBarStyle by remember { mutableStateOf(StatusBarStyle.LIGHT) }
var statusBarDropdownExpanded by remember { mutableStateOf(false) }
var enableOfflineCache by remember { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current
val scrollState = rememberScrollState()

Expand All @@ -183,42 +185,30 @@ fun HomeScreen(

Scaffold(
topBar = {
// TopAppBar with blurred glass effect
Box {
// Background blur layer (simulated with semi-transparent surface)
Surface(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.blur(16.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f)
) {}

CenterAlignedTopAppBar(
title = {
// "Appy" branding with Display typography
Text(
text = "Appy",
style = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.primary
)
},
actions = {
IconButton(onClick = onSettingsClick) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
CenterAlignedTopAppBar(
title = {
// "Appy" branding with Display typography
Text(
text = "Appy",
style = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.primary
)
},
actions = {
IconButton(onClick = onSettingsClick) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f)
)
}
)
}
) { paddingValues ->
Column(
Expand Down Expand Up @@ -427,6 +417,47 @@ fun HomeScreen(
}
}

// Offline Cache Toggle
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { enableOfflineCache = !enableOfflineCache }
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Offline Cache",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "Experimental",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onTertiaryContainer,
modifier = Modifier
.background(
MaterialTheme.colorScheme.tertiaryContainer,
RoundedCornerShape(4.dp)
)
.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
Text(
text = "Cache pages for offline use. May not work on all websites.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
)
}
Spacer(modifier = Modifier.width(8.dp))
Switch(
checked = enableOfflineCache,
onCheckedChange = { enableOfflineCache = it }
)
}

// Info note about customization
Row(
modifier = Modifier
Expand Down Expand Up @@ -464,7 +495,8 @@ fun HomeScreen(
appName = appName,
packageId = packageId,
iconUri = iconUri,
statusBarStyle = statusBarStyle
statusBarStyle = statusBarStyle,
enableOfflineCache = enableOfflineCache
)
)
}
Expand Down Expand Up @@ -562,11 +594,11 @@ fun BuildStatusSection(buildState: BuildState) {
AnimatedContent(
targetState = buildState,
transitionSpec = {
(fadeIn(animationSpec = tween(300, easing = emphasizedEasing)) +
scaleIn(animationSpec = tween(300, easing = emphasizedEasing), initialScale = 0.9f))
(fadeIn(animationSpec = tween(250, easing = emphasizedEasing)) +
scaleIn(animationSpec = tween(250, easing = emphasizedEasing), initialScale = 0.95f))
.togetherWith(
fadeOut(animationSpec = tween(300, easing = emphasizedEasing)) +
scaleOut(animationSpec = tween(300, easing = emphasizedEasing), targetScale = 0.9f)
fadeOut(animationSpec = tween(250, easing = emphasizedEasing)) +
scaleOut(animationSpec = tween(250, easing = emphasizedEasing), targetScale = 0.95f)
)
},
label = "buildStateTransition"
Expand All @@ -584,7 +616,7 @@ fun BuildStatusSection(buildState: BuildState) {
) {
val animatedProgress by animateFloatAsState(
targetValue = state.progress,
animationSpec = tween(300, easing = emphasizedEasing),
animationSpec = tween(250, easing = emphasizedEasing),
label = "progressAnimation"
)

Expand Down
1 change: 1 addition & 0 deletions template/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:supportsRtl="true"
Expand Down
Loading
Loading