diff --git a/app/src/main/java/com/example/firefly_go_android/MainActivity.kt b/app/src/main/java/com/example/firefly_go_android/MainActivity.kt index bff8d4b..76722e2 100644 --- a/app/src/main/java/com/example/firefly_go_android/MainActivity.kt +++ b/app/src/main/java/com/example/firefly_go_android/MainActivity.kt @@ -330,752 +330,7 @@ fun removeFile(targetDir: File, fileName: String): Boolean { } -@SuppressLint("ImplicitSamInstance") -@Composable -fun ServerControlScreen(appDataPath: String, dataDir: File, appVersion: AppVersion, authManager: AuthManager, modifier: Modifier = Modifier) { - val context = LocalContext.current - val isRunning = GolangServerService.isRunning - var showResetDialog by remember { mutableStateOf(false) } - var showUpdateDialog by remember { mutableStateOf(false) } - var showLogs by remember { mutableStateOf(false) } - Box( - modifier = modifier.fillMaxSize() - ) { - // Background image - Image( - painter = painterResource(id = R.drawable.background), - contentDescription = "Background", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.3f)) - ) - - Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Title - Text( - text = "Firefly GO 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) - ContextCompat.startForegroundService(context, intent) - } else { - context.stopService(Intent(context, GolangServerService::class.java)) - } - } 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( - modifier = Modifier.fillMaxWidth(0.85f), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically - ) { - - // Check Update widget - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .width(80.dp) - .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 - .width(80.dp) - .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 - .width(80.dp) - .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, 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)) - } - }, - dismissButton = { - TextButton( - onClick = { showResetDialog = false } - ) { - Text("No") - } - } - ) - } - - // Auto Update Dialog - if (showUpdateDialog) { - AutoUpdateDialog( - onDismiss = { showUpdateDialog = false }, - appVersion, - dataDir - ) - } -} - -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 -} - - -fun parseAnsi(text: String, defaultColor: Color): AnnotatedString { - val regex = Regex("\u001B\\[(\\d+)(;\\d+)*m") - val builder = buildAnnotatedString { - var lastIndex = 0 - var currentColor = defaultColor - - for (match in regex.findAll(text)) { - val start = match.range.first - - // 1. Thêm phần text TRƯỚC mã ANSI với màu HIỆN TẠI - val before = text.substring(lastIndex, start) - if (before.isNotEmpty()) { - withStyle(SpanStyle(color = currentColor)) { - append(before) - } - } - - // 2. Lấy mã code (ví dụ 31, 36, hoặc 0) - val code = try { - match.groupValues[1].toInt() - } catch (e: NumberFormatException) { - 0 - } - - currentColor = when (code) { - 0 -> defaultColor - 30 -> Color.Black - 31 -> Color.Red - 32 -> Color(0xFF00C853) // Green - 33 -> Color(0xFFFFD600) // Yellow - 34 -> Color(0xFF2962FF) // Blue - 35 -> Color(0xFFD500F9) // Magenta - 36 -> Color(0xFF00B8D4) // Cyan - 37 -> Color.White - else -> currentColor - } - - lastIndex = match.range.last + 1 - } - - if (lastIndex < text.length) { - val remain = text.substring(lastIndex) - if (remain.isNotEmpty()) { - withStyle(SpanStyle(color = currentColor)) { - append(remain) - } - } - } - } - - return builder -} - -@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) - } - } - - val defaultTextColor = LocalContentColor.current - - Dialog(onDismissRequest = { onDismiss() }) { - Surface( - shape = RoundedCornerShape(12.dp), - tonalElevation = 8.dp, - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.7f) - ) { - 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 = parseAnsi(logs[index], defaultTextColor), - fontSize = 12.sp, - - // 2. DÙNG FONT MONOSPACE - fontFamily = FontFamily.Monospace, - - // 3. (Tuỳ chọn) Giảm chiều cao dòng để logo liền mạch - lineHeight = 14.sp, - - 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, - dataDir: File, - isFirstOpen: Boolean = false -) { - val context = LocalContext.current - val autoUpdaterManager = AutoUpdaterManager(context) - var update by remember { mutableStateOf(null) } - var progress by remember { mutableStateOf(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" - ) - } - - val hasUpdate = result != null && appVersion.latestVersion != result.latestversion - - update = if (hasUpdate) result else null - - showDialog = if (isFirstOpen) { - hasUpdate - } else { - result != null - } - } - - - // Download progress - LaunchedEffect(progress) { - if (progress >= 100 && isDownloading) { - downloadComplete = true - } - } - - 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, - "FireflyGO_${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) - ) - } - } - } - } - } -} @Composable fun Modifier.bounceClick(