Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7722e5fa70 | |||
| dafb6aba1b | |||
| 3e967d7bed | |||
| c75207f8e1 | |||
| 3871dc8677 | |||
| 72dc9b238f |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,3 +15,4 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
.history/
|
||||
@@ -18,12 +18,12 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/Theme.FireflyPsAndorid"
|
||||
android:theme="@style/Theme.FireflyGoAndroid"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FireflyPsAndorid">
|
||||
android:theme="@style/Theme.FireflyGoAndroid">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import libandroid.Libandroid
|
||||
|
||||
class GolangServerService : Service() {
|
||||
@@ -47,20 +48,29 @@ class GolangServerService : Service() {
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
val largeIcon = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.mipmap.ic_launcher_round)
|
||||
.setLargeIcon(largeIcon)
|
||||
.setContentTitle("FireflyGO Server")
|
||||
.setContentText("FireflyGO is running...")
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentText("Server is running...")
|
||||
.setColor(ContextCompat.getColor(this, R.color.teal_700))
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setShowWhen(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.build()
|
||||
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
|
||||
try {
|
||||
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
|
||||
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "GolangServer::WakeLock")
|
||||
wakeLock?.acquire()
|
||||
wakeLock?.acquire(10*60*1000L)
|
||||
Log.d(TAG, "✅ WakeLock acquired")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ WakeLock failed", e)
|
||||
@@ -106,7 +116,6 @@ class GolangServerService : Service() {
|
||||
Log.e(TAG, "Error shutting down server", e)
|
||||
}
|
||||
|
||||
// 2. Giải phóng WakeLock nếu còn giữ
|
||||
try {
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
@@ -132,7 +141,7 @@ class GolangServerService : Service() {
|
||||
description = "Channel for running Golang backend in foreground"
|
||||
}
|
||||
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(channel)
|
||||
Log.d(TAG, "✅ Notification channel created")
|
||||
}
|
||||
|
||||
@@ -16,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.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -56,12 +55,17 @@ 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.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
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
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
data class AppVersion(
|
||||
val latestVersion: String,
|
||||
@@ -98,7 +102,7 @@ class MainActivity : ComponentActivity() {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ServerControlScreen(appDataPath, dataDir, appVersion, Modifier.padding(innerPadding))
|
||||
AutoUpdateDialog(onDismiss = {}, appVersion, true)
|
||||
AutoUpdateDialog(onDismiss = {}, appVersion, dataDir, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +130,28 @@ fun copyRawToFile(context: Context, targetDir: File, fileName: String, resId: In
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFile(targetDir: File, fileName: String): Boolean {
|
||||
val file = File(targetDir, fileName)
|
||||
return if (file.exists()) {
|
||||
try {
|
||||
if (file.delete()) {
|
||||
Log.i("FileRemove", "🗑️ Removed $fileName from ${file.absolutePath}")
|
||||
true
|
||||
} else {
|
||||
Log.e("FileRemove", "❌ Failed to remove $fileName from ${file.absolutePath}")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("FileRemove", "❌ Error removing $fileName: ${e.message}")
|
||||
false
|
||||
}
|
||||
} else {
|
||||
Log.i("FileRemove", "ℹ️ $fileName does not exist in ${targetDir.absolutePath}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ImplicitSamInstance")
|
||||
@Composable
|
||||
fun ServerControlScreen(appDataPath: String, dataDir: File, appVersion: AppVersion, modifier: Modifier = Modifier) {
|
||||
@@ -161,7 +187,7 @@ fun ServerControlScreen(appDataPath: String, dataDir: File, appVersion: AppVersi
|
||||
) {
|
||||
// Title
|
||||
Text(
|
||||
text = "Firefly Ps for Android",
|
||||
text = "Firefly GO for Android",
|
||||
fontSize = 26.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 24.dp),
|
||||
@@ -248,7 +274,6 @@ fun ServerControlScreen(appDataPath: String, dataDir: File, appVersion: AppVersi
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// Check Update widget
|
||||
Column(
|
||||
@@ -381,7 +406,9 @@ fun ServerControlScreen(appDataPath: String, dataDir: File, appVersion: AppVersi
|
||||
// Auto Update Dialog
|
||||
if (showUpdateDialog) {
|
||||
AutoUpdateDialog(
|
||||
onDismiss = { showUpdateDialog = false }, appVersion
|
||||
onDismiss = { showUpdateDialog = false },
|
||||
appVersion,
|
||||
dataDir
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -394,6 +421,63 @@ fun parseGoLogLine(line: String): String? {
|
||||
return if (content.isNullOrBlank()) null else content
|
||||
}
|
||||
|
||||
fun parseAnsi(text: String): AnnotatedString {
|
||||
val regex = Regex("\u001B\\[(\\d+)(;\\d+)*m")
|
||||
val builder = buildAnnotatedString {
|
||||
var lastIndex = 0
|
||||
var currentColor = Color.Black
|
||||
|
||||
for (match in regex.findAll(text)) {
|
||||
val start = match.range.first
|
||||
val before = text.substring(lastIndex, start)
|
||||
withStyle(SpanStyle(color = currentColor)) {
|
||||
append(before)
|
||||
}
|
||||
|
||||
val code = match.groupValues[1].toInt()
|
||||
currentColor = when (code) {
|
||||
30 -> {
|
||||
Color.Black
|
||||
}
|
||||
31 -> {
|
||||
Color.Red
|
||||
}
|
||||
32 -> {
|
||||
Color(0xFF00C853)
|
||||
}
|
||||
33 -> {
|
||||
Color(0xFFFFD600)
|
||||
}
|
||||
34 -> {
|
||||
Color(0xFF2962FF)
|
||||
}
|
||||
35 -> {
|
||||
Color(0xFFD500F9)
|
||||
}
|
||||
36 -> {
|
||||
Color(0xFF00B8D4)
|
||||
}
|
||||
|
||||
37 -> {
|
||||
Color.White
|
||||
}
|
||||
else -> {
|
||||
Color.Black
|
||||
}
|
||||
}
|
||||
|
||||
lastIndex = match.range.last + 1
|
||||
}
|
||||
|
||||
val remain = text.substring(lastIndex)
|
||||
withStyle(SpanStyle(color = currentColor)) {
|
||||
append(remain)
|
||||
}
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun LogPopup(
|
||||
@@ -444,19 +528,16 @@ fun LogPopup(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
LazyColumn(state = listState, modifier = Modifier.weight(1f)) {
|
||||
items(logs.size) { index ->
|
||||
Text(
|
||||
text = logs[index],
|
||||
text = parseAnsi(logs[index]),
|
||||
fontSize = 12.sp,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.padding(vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = { onDismiss() },
|
||||
@@ -475,6 +556,7 @@ fun LogPopup(
|
||||
fun AutoUpdateDialog(
|
||||
onDismiss: () -> Unit,
|
||||
appVersion: AppVersion,
|
||||
dataDir: File,
|
||||
isFirstOpen: Boolean = false
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -522,6 +604,10 @@ fun AutoUpdateDialog(
|
||||
LaunchedEffect(progress) {
|
||||
if (progress >= 100 && isDownloading) {
|
||||
downloadComplete = true
|
||||
|
||||
removeFile(dataDir, "data-in-game.json" )
|
||||
removeFile(dataDir, "freesr-data.json")
|
||||
removeFile(dataDir, "version.json")
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
@@ -601,7 +687,7 @@ fun AutoUpdateDialog(
|
||||
autoUpdaterManager.downloadapk(
|
||||
context,
|
||||
update!!.apk_url,
|
||||
"MyApp_${update!!.latestversion}.apk"
|
||||
"FireflyGO_${update!!.latestversion}.apk"
|
||||
) { prog -> progress = prog }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"latest_version": "3.6.2-01",
|
||||
"changelog": "UPDATE: Update to 3.6.52",
|
||||
"apk_url": "https://cloud.kain.id.vn/seafhttp/f/7a7d79a5354e4d39976f/?op=view"
|
||||
"latest_version": "3.6.2-05",
|
||||
"changelog": "UPDATE: Re-optimize performance",
|
||||
"apk_url": "https://git.kain.io.vn/Firefly-Shelter/FireflyGo_Android/releases/download/3.6.2-05/firefly_go_android.apk"
|
||||
}
|
||||
@@ -2,20 +2,20 @@
|
||||
"leader": 0,
|
||||
"lineups": {
|
||||
"0": 1413,
|
||||
"1": 1403,
|
||||
"1": 1415,
|
||||
"2": 1409,
|
||||
"3": 1407
|
||||
},
|
||||
"position": {
|
||||
"x": -30,
|
||||
"z": -22750,
|
||||
"y": -15000,
|
||||
"rot_y": 234288
|
||||
"x": 218004,
|
||||
"z": 259263,
|
||||
"y": 53915,
|
||||
"rot_y": 79863
|
||||
},
|
||||
"scene": {
|
||||
"plane_id": 10000,
|
||||
"floor_id": 10000003,
|
||||
"entry_id": 100000352
|
||||
"plane_id": 20423,
|
||||
"floor_id": 20423001,
|
||||
"entry_id": 2042301
|
||||
},
|
||||
"player_outfit": [
|
||||
1001
|
||||
@@ -145,12 +145,19 @@
|
||||
},
|
||||
"theory_craft": {
|
||||
"hp": {
|
||||
"1": 600000,
|
||||
"2": 10000000
|
||||
"1": [
|
||||
200000,
|
||||
1000000,
|
||||
200000
|
||||
],
|
||||
"2": [
|
||||
500000,
|
||||
10000000,
|
||||
500000
|
||||
]
|
||||
},
|
||||
"cycle_count": 1,
|
||||
"log": false,
|
||||
"mode": false
|
||||
"mode": true
|
||||
},
|
||||
"profile_data": {
|
||||
"cur_chat_bubble_id": 220008,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.FireflyPsAndorid" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.FireflyGoAndroid" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
@@ -15,7 +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
|
||||
android.enableJetifier=false
|
||||
# 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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5334f2dbde52cf6c2a2a0152a3b2250bc217a19036d3607ce81a9b84ff1c69d0
|
||||
size 89535421
|
||||
oid sha256:b2890a23bc1bad9326247f5b8a055f7bfe93eeaaf0e89e220fe27cf9aedf07b5
|
||||
size 89623442
|
||||
|
||||
2
script/.gitignore
vendored
Normal file
2
script/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
*.exe
|
||||
2
script/README_Note.md
Normal file
2
script/README_Note.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Changelog
|
||||
## - UPDATE: Re-optimize performance
|
||||
5
script/go.mod
Normal file
5
script/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module release
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
||||
2
script/go.sum
Normal file
2
script/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
154
script/main.go
Normal file
154
script/main.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
const (
|
||||
repoOwner = "Firefly-Shelter"
|
||||
repoName = "FireflyGo_Android"
|
||||
giteaURL = "https://git.kain.io.vn"
|
||||
)
|
||||
|
||||
type ReleaseInput struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
}
|
||||
|
||||
type ReleaseResponse struct {
|
||||
ID int `json:"id"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func readFile(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to read %s: %v", path, err))
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load("script/.env")
|
||||
if err != nil {
|
||||
fmt.Println("Error loading .env file")
|
||||
}
|
||||
|
||||
token := os.Getenv("TOKEN")
|
||||
if token == "" {
|
||||
fmt.Println("TOKEN not found in .env")
|
||||
}
|
||||
|
||||
|
||||
releaseJSON := readFile("script/release.json")
|
||||
var meta map[string]string
|
||||
if err := json.Unmarshal([]byte(releaseJSON), &meta); err != nil {
|
||||
panic("Invalid release.json")
|
||||
}
|
||||
tag := meta["tag"]
|
||||
title := meta["title"]
|
||||
body := readFile("script/README_Note.md")
|
||||
|
||||
// Step 1: Create release
|
||||
releaseInput := ReleaseInput{
|
||||
TagName: tag,
|
||||
Name: title,
|
||||
Body: body,
|
||||
Draft: false,
|
||||
Prerelease: false,
|
||||
}
|
||||
payload, _ := json.Marshal(releaseInput)
|
||||
|
||||
req, _ := http.NewRequest("POST",
|
||||
fmt.Sprintf("%s/api/v1/repos/%s/%s/releases", giteaURL, repoOwner, repoName),
|
||||
bytes.NewReader(payload),
|
||||
)
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
panic(fmt.Sprintf("Failed to create release: %s", bodyBytes))
|
||||
}
|
||||
|
||||
var releaseResp ReleaseResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&releaseResp); err != nil {
|
||||
panic("Failed to decode release response")
|
||||
}
|
||||
|
||||
fmt.Printf("Release created:\n- ID: %d\n- HTML: %s\n", releaseResp.ID, releaseResp.HTMLURL)
|
||||
|
||||
uploadURL := releaseResp.URL
|
||||
if uploadURL == "" {
|
||||
panic("url missing in release response")
|
||||
}
|
||||
|
||||
files, err := os.ReadDir("app/release")
|
||||
if err != nil {
|
||||
panic("Cannot read prebuild folder")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) != ".apk" {
|
||||
continue
|
||||
}
|
||||
uploadPath := filepath.Join("app/release", file.Name())
|
||||
fmt.Println("Uploading:", uploadPath)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(buf)
|
||||
part, err := writer.CreateFormFile("attachment", filepath.Base(uploadPath))
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create form: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
src, err := os.Open(uploadPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot open file %s: %v\n", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
io.Copy(part, src)
|
||||
src.Close()
|
||||
writer.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", uploadURL+"/assets", buf)
|
||||
if err != nil {
|
||||
fmt.Printf("NewRequest error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Upload failed: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
fmt.Printf("Upload failed: %s\n", bodyBytes)
|
||||
} else {
|
||||
fmt.Println("Uploaded:", file.Name())
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
5
script/release.json
Normal file
5
script/release.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tag": "3.6.2-05",
|
||||
"title": "PreBuild Version 3.6.52 - 05"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user