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")
+ }
}
}