diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ba2a4c..c557be1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,34 +27,61 @@ android { ) } } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } + kotlinOptions { jvmTarget = "11" } + buildFeatures { compose = true } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.0" + } } dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") + implementation("androidx.activity:activity-compose:1.8.2") + // Compose UI + implementation("androidx.compose.ui:ui:1.5.3") + implementation("androidx.compose.ui:ui-graphics:1.5.3") + implementation("androidx.compose.ui:ui-tooling-preview:1.5.3") - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.ui) - implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) - implementation(libs.androidx.material3) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) + // Foundation & Animation + implementation("androidx.compose.foundation:foundation:1.5.3") + implementation("androidx.compose.animation:animation:1.5.3") + implementation("androidx.compose.animation:animation-core:1.5.3") + + // Material & Material3 + implementation("androidx.compose.material:material:1.5.3") + implementation("androidx.compose.material:material-icons-extended:1.5.3") + implementation("androidx.compose.material3:material3:1.2.0") + implementation("androidx.compose.material3:material3-window-size-class:1.2.0") + + // Auto updater library + implementation("com.github.CSAbhiOnline:AutoUpdater:1.0.1") + + // Unit Test + testImplementation("junit:junit:4.13.2") + + // Android Instrumentation Test + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.3") + + // Debug + debugImplementation("androidx.compose.ui:ui-tooling:1.5.3") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.3") + + // Local AAR library implementation(files("../library/firefly-go.aar")) -} \ No newline at end of file +} + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b7df893..ef7174e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/fireflypsandorid/MainActivity.kt b/app/src/main/java/com/example/fireflypsandorid/MainActivity.kt index dbfbdc1..de2b6f2 100644 --- a/app/src/main/java/com/example/fireflypsandorid/MainActivity.kt +++ b/app/src/main/java/com/example/fireflypsandorid/MainActivity.kt @@ -1,8 +1,11 @@ package com.example.fireflypsandorid +import AutoUpdaterManager import android.annotation.SuppressLint +import android.content.Context import android.content.Intent import android.os.Bundle + import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity @@ -13,7 +16,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -21,12 +23,54 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.autoupdater.UpdateFeatures import com.example.fireflypsandorid.ui.theme.FireflyPsAndoridTheme +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.* +import androidx.compose.animation.* +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.material.icons.Icons +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.PlayCircleFilled +import androidx.compose.material.icons.filled.RestartAlt +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material.icons.filled.StopCircle +import androidx.compose.material.icons.rounded.AutoAwesome +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.InstallMobile +import androidx.compose.material.icons.rounded.SystemUpdate +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import kotlinx.coroutines.delay +import org.json.JSONObject + +data class AppVersion( + val latestVersion: String, + val changelog: String, + val apkUrl: String +) class MainActivity : ComponentActivity() { - private val tag = "AppInit" - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -34,121 +78,736 @@ class MainActivity : ComponentActivity() { val dataDir = File("$appDataPath/data") dataDir.mkdirs() - checkAndCreateFile(dataDir, "data-in-game.json", R.raw.data_in_game_json) - checkAndCreateFile(dataDir, "freesr-data.json", R.raw.freesr_data_json) - checkAndCreateFile(dataDir, "version.json", R.raw.version_json) + copyRawToFile(this, dataDir, "data-in-game.json", R.raw.data_in_game_json) + copyRawToFile(this, dataDir, "freesr-data.json", R.raw.freesr_data_json) + copyRawToFile(this, dataDir, "version.json", R.raw.version_json) + + val jsonString = resources.openRawResource(R.raw.app_version_json).use { input -> + input.bufferedReader().use { it.readText() } + } + + val jsonObject = JSONObject(jsonString) + val latestVersion = jsonObject.getString("latest_version") + val changelog = jsonObject.getString("changelog") + val apkUrl = jsonObject.getString("apk_url") + + val appVersion = AppVersion(latestVersion, changelog, apkUrl) enableEdgeToEdge() setContent { FireflyPsAndoridTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - ServerControlScreen(appDataPath, Modifier.padding(innerPadding)) - } - } - } - } - - private fun checkAndCreateFile(targetDir: File, fileName: String, resId: Int) { - val outFile = File(targetDir, fileName) - if (!outFile.exists()) { - try { - resources.openRawResource(resId).use { input -> - FileOutputStream(outFile).use { output -> - input.copyTo(output) + Box(modifier = Modifier.fillMaxSize()) { + ServerControlScreen(appDataPath, dataDir, appVersion, Modifier.padding(innerPadding)) + AutoUpdateDialog(onDismiss = {}, appVersion, true) } } - Log.i(tag, "✅ Copied $fileName to ${outFile.absolutePath}") - } catch (e: Exception) { - Log.e(tag, "❌ Failed to copy $fileName: ${e.message}") } - } else { - Log.i(tag, "ℹ️ $fileName already exists at ${outFile.absolutePath}") } + } + } +fun copyRawToFile(context: Context, targetDir: File, fileName: String, resId: Int, override: Boolean = false) { + val outFile = File(targetDir, fileName) + if (!outFile.exists() || override) { + try { + context.resources.openRawResource(resId).use { input -> + FileOutputStream(outFile).use { output -> + input.copyTo(output) + } + } + Log.i("FileCopy", "${if (override) "✅ Overridden" else "✅ Copied"} $fileName to ${outFile.absolutePath}") + } catch (e: Exception) { + Log.e("FileCopy", "❌ Failed to copy $fileName: ${e.message}") + } + } else { + Log.i("FileCopy", "ℹ️ $fileName already exists at ${outFile.absolutePath}") + } +} @SuppressLint("ImplicitSamInstance") @Composable -fun ServerControlScreen(appDataPath: String, modifier: Modifier = Modifier) { +fun ServerControlScreen(appDataPath: String, dataDir: File, appVersion: AppVersion, modifier: Modifier = Modifier) { val context = LocalContext.current val isRunning = GolangServerService.isRunning - val serverImage = if (isRunning) - painterResource(id = R.drawable.server_running) - else - painterResource(id = R.drawable.server_stopped) - Column( - modifier = modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally + var showResetDialog by remember { mutableStateOf(false) } + var showUpdateDialog by remember { mutableStateOf(false) } + var showLogs by remember { mutableStateOf(false) } + Box( + modifier = modifier.fillMaxSize() ) { - // Title - Text( - text = "Firefly Ps for Android", - fontSize = 26.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .padding(top = 24.dp), - color = Color(0xFF4CAF50).copy(alpha = 0.9f) // Lime Green - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Server status image + // Background image Image( - painter = serverImage, - contentDescription = null, + painter = painterResource(id = R.drawable.background), + contentDescription = "Background", contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxWidth() - .height(250.dp) - .padding(8.dp) + modifier = Modifier.fillMaxSize() ) - Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.3f)) + ) - // Toggle button - Button( - onClick = { - try { - if (!isRunning) { - val intent = Intent(context, GolangServerService::class.java) - intent.putExtra("appDataPath", appDataPath) - context.startService(intent) - } else { - context.stopService(Intent(context, GolangServerService::class.java)) + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Title + Text( + text = "Firefly Ps for Android", + fontSize = 26.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 24.dp), + color = Color.White, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = Offset(2f, 2f), + blurRadius = 4f + ) + ) + ) + + // Server status with icon + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(8.dp) + ) { + Icon( + imageVector = if (isRunning) Icons.Default.PlayCircleFilled else Icons.Default.StopCircle, + contentDescription = null, + tint = if (isRunning) Color(0xFF4CAF50) else Color.Gray, + modifier = Modifier.size(40.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = if (isRunning) "Server is running" else "Server is stopped", + fontSize = 30.sp, + color = Color.White, + fontWeight = FontWeight.Medium, + style = TextStyle( + shadow = Shadow( + color = Color.Black, + offset = Offset(1f, 1f), + blurRadius = 2f + ) + ) + ) + } + + + Spacer(modifier = Modifier.height(200.dp)) + // Toggle button + Button( + onClick = { + try { + if (!isRunning) { + val intent = Intent(context, GolangServerService::class.java) + intent.putExtra("appDataPath", appDataPath) + context.startService(intent) + } else { + context.stopService(Intent(context, GolangServerService::class.java)) + } + } catch (e: Exception) { + Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show() } - } catch (e: Exception) { - Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show() + }, + colors = ButtonDefaults.buttonColors( + containerColor = if (isRunning) Color(0xFFB71C1C) else Color(0xFF2196F3), + contentColor = Color.White + ), + shape = RoundedCornerShape(12.dp), + modifier = Modifier + .fillMaxWidth(0.8f) + .height(50.dp) + ) { + Icon( + imageVector = if (isRunning) Icons.Default.Stop else Icons.Default.PlayArrow, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = if (isRunning) "Stop Server" else "Start Server", + fontSize = 20.sp + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Widget icons row + Row( + horizontalArrangement = Arrangement.spacedBy(32.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val context = LocalContext.current + + // Check Update widget + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clickable { showUpdateDialog = true } + .background( + Color.White.copy(alpha = 0.8f), + RoundedCornerShape(8.dp) + ) + .padding(12.dp) + ) { + Icon( + imageVector = Icons.Default.CloudDownload, + contentDescription = "Check Update", + tint = Color.Gray, + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Update", + fontSize = 12.sp, + color = Color.Black, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium + ) + } + + // Reset Data widget + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clickable { showResetDialog = true } + .background( + Color.White.copy(alpha = 0.8f), + RoundedCornerShape(8.dp) + ) + .padding(12.dp) + ) { + Icon( + imageVector = Icons.Default.RestartAlt, + contentDescription = "Reset Data", + tint = Color.Gray, + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Reset", + fontSize = 12.sp, + color = Color.Black, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium + ) + } + + // Logcat (Lynx) widget + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clickable { + showLogs = true // mở popup log + } + .background( + Color.White.copy(alpha = 0.8f), + RoundedCornerShape(8.dp) + ) + .padding(12.dp) + ) { + Icon( + imageVector = Icons.Default.BugReport, + contentDescription = "Open Logcat", + tint = Color.Gray, + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Logs", + fontSize = 12.sp, + color = Color.Black, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium + ) + } + } + + Spacer(modifier = Modifier.height(75.dp)) + } + } + + if (showLogs) { + LogPopup(onDismiss = { showLogs = false }) + } + // Reset Data Confirmation Dialog + if (showResetDialog) { + AlertDialog( + onDismissRequest = { showResetDialog = false }, + title = { + Text(text = "Reset Data") + }, + text = { + Text(text = "Do you want reset all data? This action can not rollback.") + }, + confirmButton = { + TextButton( + onClick = { + showResetDialog = false + try { + copyRawToFile(context, dataDir, "data-in-game.json", R.raw.data_in_game_json, true) + copyRawToFile(context, dataDir, "freesr-data.json", R.raw.freesr_data_json, true) + copyRawToFile(context, dataDir, "version.json", R.raw.version_json, true) + Toast.makeText(context, "Data has been reset successfully", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(context, "Reset failed: ${e.message}", Toast.LENGTH_SHORT).show() + } + } + ) { + Text("Yes", color = Color(0xFFFF0606)) } }, - colors = ButtonDefaults.buttonColors( - containerColor = if (isRunning) Color(0xFFB71C1C) else Color(0xFF2196F3), - contentColor = Color.White - ), + dismissButton = { + TextButton( + onClick = { showResetDialog = false } + ) { + Text("No") + } + } + ) + } + + // Auto Update Dialog + if (showUpdateDialog) { + AutoUpdateDialog( + onDismiss = { showUpdateDialog = false }, appVersion + ) + } +} + +fun parseGoLogLine(line: String): String? { + val regex = Regex(""".*GoLog\s*:?\s*(.*)""") + val match = regex.find(line) + val content = match?.groupValues?.getOrNull(1)?.trim() + + return if (content.isNullOrBlank()) null else content +} + + +@Composable +fun LogPopup( + onDismiss: () -> Unit +) { + var logs by remember { mutableStateOf(listOf()) } + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + scope.launch(Dispatchers.IO) { + try { + val process = Runtime.getRuntime().exec("logcat -s GoLog") + val reader = BufferedReader(InputStreamReader(process.inputStream)) + + var line: String? + while (reader.readLine().also { line = it } != null) { + val clean = parseGoLogLine(line!!) + if (!clean.isNullOrBlank()) { + logs = (logs + clean).takeLast(200) + } + } + } catch (e: Exception) { + logs = logs + "Error reading logcat: ${e.message}" + } + } + } + val listState = rememberLazyListState() + + LaunchedEffect(logs.size) { + if (logs.isNotEmpty()) { + listState.animateScrollToItem(logs.size - 1) + } + } + + Dialog(onDismissRequest = { onDismiss() }) { + Surface( shape = RoundedCornerShape(12.dp), + tonalElevation = 8.dp, modifier = Modifier - .fillMaxWidth(0.7f) - .height(50.dp) + .fillMaxWidth() + .fillMaxHeight(0.7f) ) { - Text( - text = if (isRunning) "Stop Server" else "Start Server", - fontSize = 20.sp + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "GoLog Output", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + + LazyColumn( + state = listState, + modifier = Modifier.weight(1f) + ) { + items(logs.size) { index -> + Text( + text = logs[index], + fontSize = 12.sp, + color = Color.Black, + modifier = Modifier.padding(vertical = 2.dp) + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { onDismiss() }, + modifier = Modifier.align(Alignment.End) + ) { + Text("Close") + } + } + } + } +} + + + +@Composable +fun AutoUpdateDialog( + onDismiss: () -> Unit, + appVersion: AppVersion, + isFirstOpen: Boolean = false +) { + val context = LocalContext.current + val autoUpdaterManager = AutoUpdaterManager(context) + var update by remember { mutableStateOf(null) } + var progress by remember { mutableIntStateOf(0) } + var showDialog by remember { mutableStateOf(false) } + var isDownloading by remember { mutableStateOf(false) } + var downloadComplete by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + + val progressAnimation by animateFloatAsState( + targetValue = progress / 100f, + animationSpec = tween(300, easing = FastOutSlowInEasing), + label = "progress" + ) + + val scaleAnimation by animateFloatAsState( + targetValue = if (showDialog) 1f else 0.8f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + label = "scale" + ) + + // Check for update + LaunchedEffect(Unit) { + val result = withContext(Dispatchers.IO) { + autoUpdaterManager.checkForUpdate( + JSONfileURL = "https://git.kain.io.vn/Firefly-Shelter/FireflyGo_Andoid/raw/branch/master/app/src/main/res/raw/app_version_json.json" ) } - Spacer(modifier = Modifier.height(4.dp)) + val hasUpdate = result != null && appVersion.latestVersion != result.latestversion - // Server status text - Text( - text = if (isRunning) "Server is running" else "Server is stopped", - fontSize = 24.sp, - color = if (isRunning) Color(0xFF4CAF50) else Color.Gray - ) + update = if (hasUpdate) result else null - Spacer(modifier = Modifier.height(24.dp)) + showDialog = if (isFirstOpen) { + hasUpdate + } else { + result != null + } + } + + + // Download progress + LaunchedEffect(progress) { + if (progress >= 100 && isDownloading) { + downloadComplete = true + delay(500) + } + } + + if (showDialog) { + Dialog( + onDismissRequest = { + if (!isDownloading) showDialog = false + onDismiss() + }, + properties = DialogProperties( + dismissOnBackPress = !isDownloading, + dismissOnClickOutside = !isDownloading + ) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .scale(scaleAnimation) + .animateContentSize(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Header icon + Box( + modifier = Modifier + .size(72.dp) + .background( + MaterialTheme.colorScheme.primaryContainer, + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = if (update != null) Icons.Rounded.SystemUpdate + else Icons.Rounded.CheckCircle, + contentDescription = null, + modifier = Modifier.size(36.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Title + Text( + text = if (update != null) "Update Available" else "No Update Available", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(12.dp)) + + if (update != null) { + VersionInfoSection(update!!) + ChangelogSection(update!!) + DownloadProgressSection( + isDownloading = isDownloading, + downloadComplete = downloadComplete, + progress = progressAnimation + ) + ActionButtons( + isDownloading = isDownloading, + downloadComplete = downloadComplete, + onDownloadClick = { + isDownloading = true + coroutineScope.launch { + withContext(Dispatchers.IO) { + autoUpdaterManager.downloadapk( + context, + update!!.apk_url, + "MyApp_${update!!.latestversion}.apk" + ) { prog -> progress = prog } + } + } + }, + onDismiss = { showDialog = false; onDismiss() } + ) + } else { + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Your app is up to date", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(24.dp)) + + + Button( + onClick = { showDialog = false; onDismiss() }, + modifier = Modifier.wrapContentWidth(), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + contentPadding = PaddingValues(horizontal = 32.dp, vertical = 12.dp) + ) { + Text( + text = "OK", + style = MaterialTheme.typography.labelMedium + ) + } + } + } + } + } + } + } +} + +// Version info card +@Composable +fun VersionInfoSection(update: UpdateFeatures) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Latest Version", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Surface( + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.primary + ) { + Text( + text = "v${update.latestversion}", + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Medium + ) + } + } + } + } + Spacer(modifier = Modifier.height(12.dp)) +} + +// Changelog section +@Composable +fun ChangelogSection(update: UpdateFeatures) { + Column(modifier = Modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Rounded.AutoAwesome, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "What's New", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = update.changelog, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 20.sp + ) + } +} + +// Progress section +@Composable +fun DownloadProgressSection( + isDownloading: Boolean, + downloadComplete: Boolean, + progress: Float +) { + if (!isDownloading && !downloadComplete) return + Spacer(modifier = Modifier.height(16.dp)) + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = if (downloadComplete) "Installation Ready" else "Downloading...", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium + ) + Text( + text = "${(progress*100).toInt()}%", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Spacer(modifier = Modifier.height(8.dp)) + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(4.dp)), + color = if (downloadComplete) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } +} + +// Action buttons +@Composable +fun ActionButtons( + isDownloading: Boolean, + downloadComplete: Boolean, + onDownloadClick: () -> Unit, + onDismiss: () -> Unit +) { + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (!isDownloading && !downloadComplete) Arrangement.spacedBy(12.dp) else Arrangement.Center + ) { + if (!downloadComplete) { + OutlinedButton( + onClick = onDismiss, + modifier = Modifier.weight(1f), + enabled = !isDownloading, + shape = RoundedCornerShape(12.dp) + ) { + Text(text = "Later", style = MaterialTheme.typography.labelLarge) + } + } + + Button( + onClick = onDownloadClick, + modifier = if (downloadComplete) Modifier.widthIn(min = 160.dp) else Modifier.weight(1f), + enabled = !isDownloading || downloadComplete, + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (downloadComplete) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary + ) + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + when { + downloadComplete -> { + Icon(Icons.Rounded.InstallMobile, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(8.dp)) + Text("Install Now", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Medium) + } + isDownloading -> { + CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary) + } + else -> { + Icon( + imageVector = Icons.Rounded.Download, + contentDescription = "Download", + modifier = Modifier.size(24.dp) + ) + } + } + } + } } } diff --git a/app/src/main/res/drawable/background.jpg b/app/src/main/res/drawable/background.jpg new file mode 100644 index 0000000..e4c790f Binary files /dev/null and b/app/src/main/res/drawable/background.jpg differ diff --git a/app/src/main/res/drawable/server_running.jpg b/app/src/main/res/drawable/server_running.jpg deleted file mode 100644 index 46023d5..0000000 Binary files a/app/src/main/res/drawable/server_running.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/server_stopped.jpg b/app/src/main/res/drawable/server_stopped.jpg deleted file mode 100644 index 8e31f3e..0000000 Binary files a/app/src/main/res/drawable/server_stopped.jpg and /dev/null differ diff --git a/app/src/main/res/raw/app_version_json.json b/app/src/main/res/raw/app_version_json.json new file mode 100644 index 0000000..28ed4d6 --- /dev/null +++ b/app/src/main/res/raw/app_version_json.json @@ -0,0 +1,5 @@ +{ + "latest_version": "3.5.2-4", + "changelog": "UPDATE: Add self update, reset data, logs", + "apk_url": "https://git.kain.io.vn/Firefly-Shelter/FireflyGo_Andoid/releases/download/3.5.2-02/firefly_go_android.apk" +} \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..59daf6a --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,3 @@ + + + diff --git a/gradle.properties b/gradle.properties index 20e2a01..68c91ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,6 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true +android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the diff --git a/library/firefly-go.aar b/library/firefly-go.aar index a5f63a5..48c9dd5 100644 --- a/library/firefly-go.aar +++ b/library/firefly-go.aar @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abad4850602cbc65eba293b5d11527f6412275c1c7e5fd4e146c59fe3cf6258d -size 73423352 +oid sha256:de74022be6eac83c759d9e55cd5a9a97575ce8dbb8e5e7ae77efce6e11988c7e +size 86853555 diff --git a/settings.gradle.kts b/settings.gradle.kts index d0a05db..fe92c71 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,9 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven{ + url=uri("https://jitpack.io") + } } }