Compare commits

...
Author SHA1 Message Date
Chloe 208819436a actually keep it missing 2025-12-17 22:59:45 -07:00
Chloe d7c9e82fc2 fix missing bean 2025-12-17 22:53:00 -07:00
Chloe 8448b3f2a9 i am curious 2025-12-17 22:44:29 -07:00
Chloe 3a6c731710 use latest gradle 2025-12-17 16:16:21 -07:00
Chloe 2dde48c8fc update dependencies pt 1 2025-12-17 02:34:04 -07:00
Chloe f52b90b259 vnm 2025-09-29 01:06:44 -07:00
Chloe fa69a649dd always force hrr 2025-09-20 18:49:45 -07:00
Chloe a16c76cd06 color tweaks 2025-09-20 18:47:57 -07:00
Chloe 9b5d614d1a fix fit to screen background color 2025-09-20 18:28:29 -07:00
Chloe d6a6de8361 stop using alertdialog for settings 2025-09-20 18:23:31 -07:00
39 changed files with 1188 additions and 288 deletions

No files matched your search

+16 -9
View File
@@ -91,7 +91,7 @@ android {
}
}
namespace = "com.geode.launcher"
ndkVersion = "28.1.13356709"
ndkVersion = "29.0.14206865"
}
dependencies {
@@ -100,20 +100,27 @@ dependencies {
implementation ("androidx.compose.ui:ui")
implementation ("androidx.compose.material3:material3")
implementation ("androidx.compose.ui:ui-tooling-preview")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.3")
implementation ("androidx.activity:activity-compose:1.11.0")
implementation ("androidx.activity:activity-ktx:1.11.0")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation ("androidx.activity:activity-compose:1.12.1")
implementation ("androidx.activity:activity-ktx:1.12.1")
implementation ("androidx.appcompat:appcompat:1.7.1")
implementation ("androidx.documentfile:documentfile:1.1.0")
implementation ("com.squareup.okio:okio:3.16.0")
implementation ("com.squareup.okhttp3:okhttp:5.1.0")
implementation ("com.squareup.okio:okio:3.16.4")
implementation ("com.squareup.okhttp3:okhttp:5.3.2")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.9.0")
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.37.0")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.37.0")
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.38.1")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.38.1")
implementation ("androidx.browser:browser:1.9.0")
implementation ("io.ktor:ktor-server-core-jvm:3.3.3")
implementation ("io.ktor:ktor-server-cio:3.3.3")
implementation ("io.ktor:ktor-utils-jvm:3.3.3")
implementation ("io.ktor:ktor-server-status-pages:3.3.3")
implementation ("io.ktor:ktor-server-sse:3.3.3")
debugImplementation ("androidx.compose.ui:ui-tooling")
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
}
+1
View File
@@ -5,3 +5,4 @@
# note: if you're going to add more rules, consider the @Keep annotation
# this should really only be kept to non-custom code
-dontwarn java.lang.management.**
+8
View File
@@ -22,6 +22,8 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-feature
android:name="android.hardware.wifi"
@@ -156,6 +158,12 @@
<meta-data
android:name="android.game_mode_config"
android:resource="@xml/game_mode_config" />
<service
android:name=".DeveloperServerService"
android:foregroundServiceType="dataSync"
android:exported="false">
</service>
</application>
</manifest>
@@ -0,0 +1,376 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Geode Android Web Interface</title>
<meta name="color-scheme" content="light dark">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Quicksand:wght@300..700&display=swap" rel="stylesheet">
<style>
.logo {
width: 1em;
height: 1em;
}
.title {
font-family: "Quicksand", sans-serif;
font-weight: 600;
font-size: 2rem;
display: flex;
align-items: center;
gap: 0.3em;
}
.listing-main {
display: flex;
}
body {
font-family: "Poppins", sans-serif;
}
#file-name {
font-family: "Quicksand", sans-serif;
font-weight: 600;
}
.hidden {
display: none;
}
#logs-area {
font-family: monospace;
font-size: 0.9em;
height: 15rem;
overflow-y: auto;
}
</style>
</head>
<body>
<h1 class="title">
<svg
width="698"
height="698"
viewBox="0 0 696 698"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="logo"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M393.076 24.4223L372.884 9.00248C358.192 -2.21718 337.808 -2.2172 323.116 9.00246L16.9433 242.812C3.28395 253.243 -2.41995 271.086 2.656 287.506L120.413 668.435C125.723 685.612 141.605 697.326 159.584 697.326H536.416C554.395 697.326 570.277 685.612 575.587 668.435L693.344 287.506C698.42 271.086 692.716 253.243 679.057 242.812L650.835 221.26L618.374 323.385L529.198 613.394C525.713 624.725 515.245 632.458 503.39 632.458H190.61C178.755 632.458 168.287 624.725 164.802 613.394L67.1963 295.971C63.8815 285.191 67.6162 273.487 76.562 266.619L330.558 71.6226C331.579 70.8386 332.643 70.1371 333.739 69.5183L393.076 24.4223Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M336.614 143.264C344.511 137.209 355.489 137.209 363.386 143.264L552.797 288.49C560.096 294.086 563.143 303.632 560.437 312.422L487.62 548.924C484.778 558.153 476.251 564.451 466.594 564.451H233.406C223.749 564.451 215.222 558.153 212.38 548.924L199.934 508.5H261.676C262.393 508.647 263.134 508.724 263.888 508.724H432.112C436.963 508.724 441.241 505.546 442.643 500.903L495.141 326.982C496.452 322.638 494.961 317.934 491.386 315.139L354.777 208.3C350.795 205.186 345.205 205.186 341.223 208.3L215.925 306.292L152.151 353.307L139.563 312.422C136.857 303.632 139.904 294.086 147.203 288.49L336.614 143.264Z"
fill="currentColor"
/>
</svg>
Geode Android Manager
</h1>
<div class="listing-main">
<section>
<label for="path-listing">
Directory Listing:
</label> <br />
<select id="path-listing" size=25>
</select>
<br />
<label for="path-name">Path:</label>
<input type="text" id="path-name" />
<button id="traverse-btn">Go</button>
<!--br />
<input type="file" id="upload-file">
<button id="upload-btn">Upload File</button-->
</section>
<section>
<h2><span id="file-name"></span></h2>
<div class="hidden" id="file-metadata">
<p>
Size: <span id="file-size"></span> <br />
Modified At: <span id="file-modified"></span> <br />
</p>
<p>
<!-- button id="file-replace">Replace</button>
<button id="file-delete">Delete</button>
<br /-->
<a href="" download id="file-download">Download</a>
</p>
</div>
</section>
</div>
<div>
<h2>Logs</h2>
<pre id="logs-area"></pre>
</div>
<script>
const basePath = "/";
/** @type {HTMLSelectElement | null} */
const pathListing = document.querySelector("#path-listing");
/** @type {HTMLInputElement | null} */
const pathName = document.querySelector("#path-name");
/** @type {HTMLButtonElement | null} */
const goBtn = document.querySelector("#traverse-btn");
/** @type {HTMLSpanElement | null} */
const fileName = document.querySelector("#file-name");
/** @type {HTMLSpanElement | null} */
const fileSize = document.querySelector("#file-size");
/** @type {HTMLSpanElement | null} */
const fileModified = document.querySelector("#file-modified");
/** @type {HTMLAnchorElement | null} */
const fileDownload = document.querySelector("#file-download");
/** @type {HTMLDivElement | null} */
const fileMetadata = document.querySelector("#file-metadata");
/** @type {HTMLPreElement | null} */
const logsArea = document.querySelector("#logs-area");
const currentListing = {};
function formatBytes(bytes, decimals = 2) {
if (bytes == 0) return "0 B";
const base = 1024;
const units = ["B", "KB", "MB", "GB", "TB"];
const unitIndex = Math.floor(Math.log(bytes) / Math.log(base));
const formattedValue = (bytes / Math.pow(base, unitIndex)).toFixed(decimals);
return `${formattedValue} ${units[unitIndex]}`;
};
async function fetchDirectoryListing(path) {
goBtn.disabled = true;
try {
const data = await fetch(`${basePath}files/${path}`);
const json = await data.json();
const selectElements = json.sort((a, b) => {
const aIsDir = a.type == "directory";
const bIsDir = b.type == "directory";
if (aIsDir && !bIsDir) {
return -1;
}
if (!aIsDir && bIsDir) {
return 1;
}
return a.filename.localeCompare(b.filename);
}).map((f) => {
currentListing[f.filename] = f;
const optionElement = document.createElement("option");
optionElement.value = f.filename;
if (f.type == "directory") {
optionElement.innerText = "📁 " + f.filename;
} else {
optionElement.innerText = f.filename;
}
return optionElement;
});
const parentElement = document.createElement("option");
parentElement.value = "..";
parentElement.innerText = "⤴️ ..";
selectElements.unshift(parentElement);
pathListing.replaceChildren(...selectElements);
pathName.value = path;
} catch (e) {
console.error(e);
} finally {
goBtn.disabled = false;
}
}
function updateFileData() {
if (pathListing.value == "..") {
return;
}
const currentFile = currentListing[pathListing.value];
fileName.innerText = currentFile.filename;
if (currentFile.type == "directory") {
fileMetadata.classList.add("hidden");
return;
}
fileMetadata.classList.remove("hidden");
fileSize.innerText = formatBytes(currentFile.size);
const modifiedDate = new Date(currentFile.lastModified);
fileModified.innerText = new Intl.DateTimeFormat("default", {
timeStyle: "long",
dateStyle: "long",
}).format(modifiedDate);
fileDownload.href = `${basePath}files${updatePath(currentFile.filename)}`;
}
function updatePath(append) {
const currentPath = pathName.value;
if (append == "..") {
if (currentPath == "/") {
return "/"
} else {
const path = currentPath.split("/");
path.pop();
return path.join("/");
}
}
if (currentPath.endsWith("/")) {
return currentPath + append;
} else {
return `${currentPath}/${append}`;
}
}
pathListing.addEventListener("dblclick", async (event) => {
const clickedOption = event.target.value;
if (clickedOption == "..") {
const newPath = updatePath("..");
await fetchDirectoryListing(newPath);
return;
}
const selected = currentListing[clickedOption];
if (selected.type == "directory") {
const newPath = updatePath(selected.filename);
await fetchDirectoryListing(newPath);
} else {
fileDownload.click();
}
});
pathListing.addEventListener("change", (event) => {
const clickedOption = event.target.value;
updateFileData();
});
goBtn.addEventListener("click", async () => {
const goto = pathName.value;
await fetchDirectoryListing(goto);
});
function formatLog(line) {
/*
{
"process": {
"processId": 31123,
"threadId": 31123,
"processUid": 10486
},
"time": "2025-12-17T10:38:19.488696104Z",
"logId": 4,
"priority": "error",
"tag": "AndroidRuntime",
"message": "..."
},
*/
return `[${line.priority.charAt(0)}] ${line.time} - [${line.tag}] ${line.message}`
}
async function updateLogs() {
const data = await fetch(`${basePath}logs`);
const json = await data.json();
const lines = json.map((l) => formatLog(l));
const message = lines.join("\n");
logsArea.innerText = message;
logsArea.scrollTop = logsArea.scrollHeight - logsArea.clientHeight
}
const unprocessedLogs = [];
function onTick() {
const batch = unprocessedLogs.splice(0, 100);
if (batch.length == 0) {
return;
}
const formatted = batch.map((l) => formatLog(JSON.parse(l))).join("\n");
const isScrolledToBottom = (logsArea.scrollHeight - logsArea.clientHeight) <= logsArea.scrollTop + 1;
logsArea.innerText += `\n${formatted}`;
if (isScrolledToBottom) {
logsArea.scrollTop = logsArea.scrollHeight - logsArea.clientHeight
}
}
function animationFrame() {
onTick();
requestAnimationFrame(animationFrame);
}
function openLogsStream() {
const evtSource = new EventSource(`${basePath}logs_live`);
// we store new logs to process on the next frame, to avoid slowdowns when lots of logs come in at once
evtSource.addEventListener("log", (event) => {
unprocessedLogs.push(event.data);
});
}
document.addEventListener("DOMContentLoaded", async (event) => {
await fetchDirectoryListing("/");
await updateLogs();
openLogsStream();
requestAnimationFrame(animationFrame);
});
</script>
</body>
</html>
@@ -24,7 +24,7 @@ object BaseRobTopActivity {
var isPaused = false
lateinit var me: WeakReference<Activity>
var me: WeakReference<Activity> = WeakReference(null)
private var shouldResumeSound = true
fun setCurrentActivity(currentActivity: Activity) {
@@ -22,12 +22,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
@@ -137,8 +131,8 @@ fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifi
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(Icons.Default.Warning, contentDescription = null, modifier)
else -> Icon(Icons.Default.Info, contentDescription = null, modifier)
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(painterResource(R.drawable.icon_warning), contentDescription = null, modifier)
else -> Icon(painterResource(R.drawable.icon_info), contentDescription = null, modifier)
}
}
@@ -285,7 +279,7 @@ fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
LaunchViewModel.LaunchCancelReason.MANUAL,
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
Icon(
Icons.Filled.PlayArrow,
painterResource(R.drawable.icon_play_arrow),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@@ -293,7 +287,7 @@ fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
}
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
Icon(
Icons.Filled.Refresh,
painterResource(R.drawable.icon_refresh),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@@ -398,7 +392,7 @@ fun ExtraOptions(onSettings: () -> Unit) {
onClick = onSettings,
) {
Icon(
Icons.Filled.Settings,
painterResource(R.drawable.icon_settings),
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
)
}
@@ -0,0 +1,351 @@
package com.geode.launcher
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.geode.launcher.log.LogLine
import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okio.Buffer
import okio.buffer
import okio.source
import java.io.EOFException
import java.io.File
import java.io.IOException
import java.nio.file.DirectoryNotEmptyException
import kotlin.collections.plusAssign
import kotlin.io.path.deleteExisting
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Instant
import io.ktor.http.*
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.engine.*
import io.ktor.server.cio.*
import io.ktor.server.http.content.staticFiles
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.request.receiveChannel
import io.ktor.server.request.receiveMultipart
import io.ktor.server.sse.*
import io.ktor.sse.ServerSentEvent
import io.ktor.util.cio.writeChannel
import io.ktor.utils.io.copyAndClose
enum class FileType {
@SerialName("file")
FILE,
@SerialName("directory")
DIRECTORY
}
@Serializable
data class FileMeta(val filename: String, val type: FileType, val size: Long, val lastModified: Instant)
class DeveloperServerService : Service() {
private val binder = LocalBinder()
private var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
inner class LocalBinder : Binder() {
fun getService(): DeveloperServerService = this@DeveloperServerService
}
private fun startForeground() {
try {
val notification = NotificationCompat.Builder(this, "geode_developer_server")
.setSmallIcon(R.drawable.geode_monochrome)
.build()
ServiceCompat.startForeground(
this,
100,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
} catch (e: Exception) {
/*
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& e is ForegroundServiceStartNotAllowedException
) {
println("idk")
}
*/
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onTimeout(startId: Int, fgsType: Int) {
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
server?.stop()
server = null
}
// not recursive copy
private fun copyIndexToCache() {
val folderName = "web_interface"
val base = File(application.cacheDir, folderName)
base.deleteRecursively()
base.mkdirs()
assets.list(folderName)?.forEach {
val assetName = "$folderName/$it"
val outputPath = File(base, it)
assets.open(assetName).use { inputStream ->
outputPath.outputStream().use { outputStream ->
DownloadUtils.copyFile(inputStream, outputStream)
}
}
}
}
private fun createLogsFlow(): Flow<LogLine> = flow {
val timestamp = System.currentTimeMillis()
val seconds = timestamp / 1000
val ms = timestamp % 1000
// Time format is 'MM-DD hh:mm:ss.mmm...', 'YYYY-MM-DD hh:mm:ss.mmm...', or 'sssss.mmm...'.
val timeString = "$seconds.$ms"
val logCommand = "logcat -B -T $timeString"
val logProcess = try {
Runtime.getRuntime().exec(logCommand)
} catch (ioe: IOException) {
ioe.printStackTrace()
emit(LogLine.showException(ioe))
return@flow
}
val logBuffer = logProcess.inputStream.source().buffer()
try {
// this runs until the stream is exhausted
while (currentCoroutineContext().isActive) {
val line = LogLine.fromBufferedSource(logBuffer)
emit(line)
yield()
}
} catch (_: EOFException) {
// ignore, end of file reached
} catch (e: Exception) {
e.printStackTrace()
emit(LogLine.showException(e))
}
}
private suspend fun readLogs(): List<LogLine> {
val logLines = mutableListOf<LogLine>()
val logCommand = "logcat -B -d"
val logProcess = try {
withContext(Dispatchers.IO) {
Runtime.getRuntime().exec(logCommand)
}
} catch (ioe: IOException) {
ioe.printStackTrace()
logLines += LogLine.showException(ioe)
return logLines
}
// read entire log into a buffer so no logs are added to the buffer during processing
val logBuffer = Buffer()
logProcess.inputStream.source().buffer().readAll(logBuffer)
try {
coroutineScope {
// this runs until the stream is exhausted
while (true) {
val line = LogLine.fromBufferedSource(logBuffer)
logLines += line
yield()
}
}
} catch (_: EOFException) {
// ignore, end of file reached
} catch (e: Exception) {
e.printStackTrace()
logLines += LogLine.showException(e)
}
return logLines
}
override fun onCreate() {
super.onCreate()
val staticDir = File(application.cacheDir, "web_interface")
val baseDir = LaunchUtils.getBaseDirectory(application, true)
val logsFlow = createLogsFlow()
server = embeddedServer(CIO, port = 9043) {
routing {
install(SSE)
install(StatusPages) {
exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause" , status = HttpStatusCode.InternalServerError)
}
}
get("/logs") {
val logs = Json.encodeToString(readLogs())
call.respondText(logs, contentType = ContentType.Application.Json)
}
post("/stop") {
stopSelf()
}
sse("/logs_live") {
heartbeat {
period = 5000.milliseconds
event = ServerSentEvent(comments = "heartbeat")
}
logsFlow.collect {
val log = Json.encodeToString(it)
send(log, event = "log", id = it.logId.toString())
}
}
staticFiles("/", staticDir)
staticFiles("/files", baseDir, index = null) {
fallback { requestedPath, call ->
val basePath = baseDir.toPath()
val builtPath = basePath.resolve(requestedPath).normalize()
if (!builtPath.startsWith(basePath) || !builtPath.exists()) {
// block path traversal issues
call.response.status(HttpStatusCode.NotFound)
} else {
val filenames = builtPath.toFile().listFiles()?.map {
FileMeta(
filename = it.name,
type = if (it.isDirectory) FileType.DIRECTORY else FileType.FILE,
size = it.length(),
lastModified = Instant.fromEpochMilliseconds(it.lastModified())
)
} ?: emptyList()
val data = Json.encodeToString(filenames)
call.respondText(data, contentType = ContentType.Application.Json)
}
}
}
delete("/files/{path...}") {
val pathSegments = call.parameters.getAll("path")
val requestedPath = pathSegments?.joinToString("/") ?: ""
val basePath = baseDir.toPath()
val builtPath = basePath.resolve(requestedPath).normalize()
if (!builtPath.startsWith(basePath) || !builtPath.exists()) {
call.response.status(HttpStatusCode.NotFound)
} else {
try {
builtPath.deleteExisting()
call.response.status(HttpStatusCode.NoContent)
} catch (e: DirectoryNotEmptyException) {
val message = Json.encodeToString(buildJsonObject {
put("message", "Provided directory $requestedPath was not empty")
})
call.respondText(message, status = HttpStatusCode.Conflict, contentType = ContentType.Application.Json)
}
}
}
put("/files/{path...}") {
val pathSegments = call.parameters.getAll("path")
val requestedPath = pathSegments?.joinToString("/") ?: ""
val basePath = baseDir.toPath()
val builtPath = basePath.resolve(requestedPath).normalize()
if (!builtPath.startsWith(basePath)) {
call.response.status(HttpStatusCode.NotFound)
} else {
if (builtPath.isDirectory()) {
val multipart = call.receiveMultipart(formFieldLimit = 1024 * 1024 * 100)
multipart.forEachPart { part ->
if (part is PartData.FileItem) {
val fileName = part.originalFileName ?: "uploaded_file"
val outputPath = builtPath.resolve(fileName).normalize()
if (!outputPath.startsWith(basePath)) {
val outputFile = outputPath.toFile()
part.provider().copyAndClose(outputFile.writeChannel())
}
}
part.dispose()
}
} else {
val outputFile = builtPath.toFile()
outputFile.parentFile?.mkdirs()
call.receiveChannel().copyAndClose(outputFile.writeChannel())
}
call.response.status(HttpStatusCode.NoContent)
}
}
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
this.startForeground()
copyIndexToCache()
server?.start(wait = false)
return START_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
stopSelf()
}
}
@@ -465,8 +465,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val limitedRefreshRate = preferenceUtils.getInt(PreferenceUtils.Key.LIMIT_FRAME_RATE).takeIf { it != 0 }
mLimitedRefreshRate = limitedRefreshRate
mForceRefreshRate = preferenceUtils.getBoolean(PreferenceUtils.Key.FORCE_HRR)
if (mForceRefreshRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
mForceRefreshRate = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val chosenDisplay = display.supportedModes?.maxByOrNull { it.refreshRate }
if (chosenDisplay != null) {
var chosenRefreshRate = chosenDisplay.refreshRate
@@ -1,6 +1,9 @@
package com.geode.launcher
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -9,8 +12,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -27,18 +28,18 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.main.*
import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.launcherTitleStyle
import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import com.geode.launcher.utils.GamePackageUtils
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -63,6 +64,23 @@ class MainActivity : ComponentActivity() {
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
} else { null }
if (PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.DEVELOPER_SERVER)) {
val intent = Intent(this, DeveloperServerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = getString(R.string.notification_channel_development_server)
val descriptionText = getString(R.string.notification_channel_development_server_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val mChannel = NotificationChannel("geode_developer_server", name, importance)
mChannel.description = descriptionText
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(mChannel)
this.startForegroundService(intent)
}
}
val redirectToAlt = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
if (redirectToAlt) {
val launchIntent = Intent(this, AltMainActivity::class.java)
@@ -160,11 +178,11 @@ fun MainScreen(
) {
Image(
painter = painterResource(id = R.drawable.geode_logo),
contentDescription = context.getString(R.string.launcher_logo_alt),
contentDescription = stringResource(R.string.launcher_logo_alt),
modifier = Modifier.size(100.dp, 100.dp)
)
Text(
context.getString(R.string.launcher_title),
stringResource(R.string.launcher_title),
style = launcherTitleStyle,
modifier = Modifier.padding(12.dp)
)
@@ -199,7 +217,7 @@ fun MainScreen(
}
gdInstalled -> {
Text(
context.getString(R.string.geode_download_title),
stringResource(R.string.geode_download_title),
modifier = Modifier.padding(12.dp)
)
@@ -210,16 +228,16 @@ fun MainScreen(
) {
Icon(
painterResource(R.drawable.icon_download),
contentDescription = context.getString(R.string.launcher_download_icon_alt)
contentDescription = stringResource(R.string.launcher_download_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(context.getString(R.string.launcher_download))
Text(stringResource(R.string.launcher_download))
}
Spacer(Modifier.size(2.dp))
IconButton(onClick = { onSettings(context) }) {
Icon(
Icons.Filled.Settings,
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
painterResource(R.drawable.icon_settings),
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
)
}
}
@@ -45,7 +45,6 @@ class CrashViewModel(private val application: Application) : ViewModel() {
private var logJob: Job? = null
@OptIn(ExperimentalTime::class)
private fun getCrashList(): List<CrashDump> {
val crashDirectory = LaunchUtils.getCrashDirectory(application)
if (!crashDirectory.exists()) {
@@ -4,6 +4,9 @@ import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import okio.Buffer
import okio.BufferedSource
import java.io.IOException
@@ -26,10 +29,29 @@ fun BufferedSource.readCString(): String {
return buffer.toString()
}
@Serializable
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
@Serializable
enum class LogPriority {
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
@SerialName("unknown")
UNKNOWN,
@SerialName("default")
DEFAULT,
@SerialName("verbose")
VERBOSE,
@SerialName("debug")
DEBUG,
@SerialName("info")
INFO,
@SerialName("warn")
WARN,
@SerialName("error")
ERROR,
@SerialName("fatal")
FATAL,
@SerialName("silent")
SILENT;
class LogPriorityIterator(
start: LogPriority,
@@ -116,7 +138,8 @@ enum class LogPriority {
/**
* Represents a log entry from logcat.
*/
data class LogLine @OptIn(ExperimentalTime::class) constructor(
@Serializable
data class LogLine(
val process: ProcessInformation,
val time: Instant,
val logId: Int,
@@ -198,11 +221,10 @@ data class LogLine @OptIn(ExperimentalTime::class) constructor(
time = time,
logId = lid,
tag = tag,
message = message
message = message.trimEnd('\u0000')
)
}
@OptIn(ExperimentalTime::class)
fun showException(exception: Exception) = LogLine(
process = ProcessInformation(0, 0, 0),
time = Clock.System.now(),
@@ -213,15 +235,15 @@ data class LogLine @OptIn(ExperimentalTime::class) constructor(
)
}
@OptIn(ExperimentalTime::class)
@Transient
val identifier = time.toJavaInstant()
val messageTrimmed by lazy {
this.message.trim { it <= ' ' }
}
@OptIn(ExperimentalTime::class)
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
val asSimpleString by lazy {
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
}
@@ -18,8 +18,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -241,7 +239,7 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
} else {
Button(onClick = { onShareCrash(context) }) {
Icon(
Icons.Filled.Share,
painterResource(R.drawable.icon_share),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@@ -10,10 +10,6 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
@@ -34,6 +30,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -71,7 +68,7 @@ fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
AlertDialog(
icon = {
Icon(
Icons.Filled.Warning,
painterResource(R.drawable.icon_warning),
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
)
},
@@ -116,7 +113,7 @@ fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
AlertDialog(
icon = {
Icon(
Icons.Filled.Warning,
painterResource(R.drawable.icon_warning),
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
)
},
@@ -243,17 +240,17 @@ fun PlayButton(
}
) {
Icon(
Icons.Filled.PlayArrow,
contentDescription = context.getString(R.string.launcher_launch_icon_alt)
painterResource(R.drawable.icon_play_arrow),
contentDescription = stringResource(R.string.launcher_launch_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(context.getString(R.string.launcher_launch))
Text(stringResource(R.string.launcher_launch))
}
Spacer(Modifier.size(2.dp))
IconButton(onClick = { onSettings(context) }) {
Icon(
Icons.Filled.Settings,
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
painterResource(R.drawable.icon_settings),
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
)
}
}
@@ -295,7 +292,7 @@ fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
InlineDialog(
headline = {
Icon(
Icons.Default.Warning,
painterResource(R.drawable.icon_warning),
contentDescription = null,
modifier = Modifier.size(28.dp)
)
@@ -13,9 +13,6 @@ import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
@@ -199,7 +196,7 @@ fun LauncherUpdateContent(modifier: Modifier = Modifier) {
},
leadingContent = {
Icon(
Icons.Filled.Info,
painterResource(R.drawable.icon_info),
contentDescription = null,
)
},
@@ -232,7 +229,7 @@ fun OutdatedVersionContent(modifier: Modifier = Modifier) {
},
leadingContent = {
Icon(
Icons.Filled.Warning,
painterResource(R.drawable.icon_warning),
contentDescription = null,
)
},
@@ -251,7 +248,7 @@ fun UpdateFailedContent(modifier: Modifier = Modifier) {
},
leadingContent = {
Icon(
Icons.Filled.Warning,
painterResource(R.drawable.icon_warning),
contentDescription = null,
)
},
@@ -17,8 +17,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedCard
@@ -320,7 +318,7 @@ fun UpdateMessageIndicator(
},
) {
Icon(
Icons.Filled.Refresh,
painterResource(R.drawable.icon_refresh),
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@@ -21,8 +21,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -44,6 +42,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -162,7 +161,7 @@ fun ApplicationIconScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
@@ -18,11 +18,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -53,6 +48,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -189,6 +185,7 @@ fun ApplicationLogsScreen(
val isLoading by logViewModel.isLoading.collectAsState()
val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
@@ -236,7 +233,7 @@ fun ApplicationLogsScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
@@ -244,7 +241,7 @@ fun ApplicationLogsScreen(
actions = {
IconButton(onClick = {
coroutineScope.launch {
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
saveLauncher.launch(resources.getString(R.string.application_logs_default_filename))
}
}, enabled = !logLines.isEmpty()) {
Icon(
@@ -254,7 +251,7 @@ fun ApplicationLogsScreen(
}
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon(
Icons.Filled.MoreVert,
painterResource(R.drawable.icon_more_vert),
contentDescription = stringResource(R.string.application_logs_more)
)
}
@@ -273,7 +270,7 @@ fun ApplicationLogsScreen(
trailingIcon = {
if (logViewModel.filterCrashes) {
Icon(
Icons.Filled.Check,
painterResource(R.drawable.icon_check),
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
)
}
@@ -25,11 +25,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -169,7 +164,7 @@ fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Mo
shareCrash(context, crashDump.filename)
}) {
Icon(
Icons.Filled.Share,
painterResource(R.drawable.icon_share),
contentDescription = stringResource(R.string.application_logs_share)
)
}
@@ -178,7 +173,7 @@ fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Mo
crashViewModel.removeCrash(crashDump.filename)
}) {
Icon(
Icons.Filled.Delete,
painterResource(R.drawable.icon_delete),
contentDescription = stringResource(R.string.application_crashes_delete_one)
)
}
@@ -207,7 +202,7 @@ fun CrashLogsScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
@@ -215,7 +210,7 @@ fun CrashLogsScreen(
actions = {
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon(
Icons.Filled.MoreVert,
painterResource(R.drawable.icon_more_vert),
contentDescription = stringResource(R.string.application_logs_more)
)
}
@@ -20,9 +20,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
@@ -44,6 +41,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.geode.launcher.R
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
@@ -114,7 +112,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
@@ -161,13 +159,14 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
}}
)
/*
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
SettingsCard(
title = stringResource(R.string.preference_force_hrr),
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
)
}
/*
SettingsCard(
title = stringResource(R.string.preference_override_exceptions_name),
description = stringResource(R.string.preference_override_exceptions_description),
@@ -194,6 +193,18 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
description = LaunchUtils.getBaseDirectory(context).path,
onClick = { onOpenFolder(context) }
)
SettingsCard(
title = stringResource(R.string.preference_development_server_name),
description = stringResource(R.string.preference_development_server_description),
preferenceKey = PreferenceUtils.Key.DEVELOPER_SERVER,
) { enabled ->
if (enabled) {
} else {
}
}
}
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
@@ -21,9 +21,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -37,6 +34,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@@ -123,6 +121,7 @@ fun UpdateIndicator(
updateStatus: ReleaseViewModel.ReleaseUIState
) {
val context = LocalContext.current
val resources = LocalResources.current
var enablePopup by remember { mutableStateOf(false) }
@@ -158,8 +157,8 @@ fun UpdateIndicator(
is ReleaseViewModel.ReleaseUIState.Failure -> {
val message = when (updateStatus.exception) {
is UnknownHostException, is ConnectException ->
context.getString(R.string.release_fetch_no_internet)
else -> context.getString(R.string.preference_check_for_updates_failed)
resources.getString(R.string.release_fetch_no_internet)
else -> resources.getString(R.string.preference_check_for_updates_failed)
}
snackbarHostState.showSnackbar(message)
@@ -167,17 +166,17 @@ fun UpdateIndicator(
is ReleaseViewModel.ReleaseUIState.Finished -> {
if (updateStatus.hasUpdated) {
snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_success)
resources.getString(R.string.preference_check_for_updates_success)
)
} else {
val gameVersion = GamePackageUtils.getGameVersionCodeOrNull(context.packageManager)
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE) {
snackbarHostState.showSnackbar(
context.getString(R.string.launcher_game_update_required_short)
resources.getString(R.string.launcher_game_update_required_short)
)
} else {
snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_none_found)
resources.getString(R.string.preference_check_for_updates_none_found)
)
}
}
@@ -218,7 +217,7 @@ fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
AlertDialog(
icon = {
Icon(
Icons.Filled.Warning,
painterResource(R.drawable.icon_warning),
contentDescription = null,
)
},
@@ -245,6 +244,7 @@ fun SettingsScreen(
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) {
val context = LocalContext.current
val resources = LocalResources.current
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
val updateStatus by releaseViewModel.uiState.collectAsState()
@@ -255,7 +255,7 @@ fun SettingsScreen(
if (showUpdateInProgress) {
LaunchedEffect(snackbarHostState) {
snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_already_updating)
resources.getString(R.string.preference_check_for_updates_already_updating)
)
showUpdateInProgress = false
}
@@ -280,14 +280,14 @@ fun SettingsScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = context.getString(R.string.back_icon_alt)
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
title = {
Text(
context.getString(R.string.title_activity_settings),
stringResource(R.string.title_activity_settings),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -304,7 +304,7 @@ fun SettingsScreen(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OptionsGroup(context.getString(R.string.preference_category_testing)) {
OptionsGroup(stringResource(R.string.preference_category_testing)) {
SettingsSelectCard(
title = stringResource(R.string.preference_theme_name),
dialogTitle = stringResource(R.string.preference_theme_select),
@@ -361,8 +361,8 @@ fun SettingsScreen(
OptionsGroup(stringResource(R.string.preference_category_gameplay)) {
SettingsCard(
title = context.getString(R.string.preference_load_automatically_name),
description = context.getString(R.string.preference_load_automatically_description),
title = stringResource(R.string.preference_load_automatically_name),
description = stringResource(R.string.preference_load_automatically_description),
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
)
SettingsSelectCard(
@@ -407,7 +407,7 @@ fun SettingsScreen(
step = 5,
) {
SettingsCard(
title = context.getString(R.string.preference_screen_zoom_fit_name),
title = stringResource(R.string.preference_screen_zoom_fit_name),
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM_FIT,
asCard = false
)
@@ -430,9 +430,9 @@ fun SettingsScreen(
)
}
OptionsGroup(context.getString(R.string.preference_category_updater)) {
OptionsGroup(stringResource(R.string.preference_category_updater)) {
SettingsCard(
title = context.getString(R.string.preference_update_automatically_name),
title = stringResource(R.string.preference_update_automatically_name),
preferenceKey = PreferenceUtils.Key.UPDATE_AUTOMATICALLY,
)
OptionsCard(
@@ -538,7 +538,7 @@ fun SettingsScreen(
}
val gameSource = remember {
context.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
resources.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
GamePackageUtils.GameSource.GOOGLE -> R.string.preference_game_source_google
GamePackageUtils.GameSource.AMAZON -> R.string.preference_game_source_amazon
else -> R.string.preference_game_source_unknown
@@ -8,6 +8,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
@@ -19,12 +20,8 @@ import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -48,6 +45,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -248,33 +246,39 @@ fun StringDialog(
) {
var enteredValue by remember { mutableStateOf(initialValue) }
AlertDialog(
BaseDialog(
onDismissRequest = { onDismissRequest() },
title = {
Text(title)
},
text = {
OutlinedTextField(
value = enteredValue,
onValueChange = {
enteredValue = filterInput?.invoke(it) ?: it
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
autoCorrectEnabled = false
),
trailingIcon = {
if (enteredValue.isNotEmpty()) {
IconButton(onClick = { enteredValue = "" }) {
Icon(
Icons.Filled.Clear,
contentDescription = stringResource(R.string.preference_text_clear)
)
title = title,
content = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = enteredValue,
onValueChange = {
enteredValue = filterInput?.invoke(it) ?: it
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Ascii,
autoCorrectEnabled = false
),
trailingIcon = {
if (enteredValue.isNotEmpty()) {
IconButton(onClick = { enteredValue = "" }) {
Icon(
painterResource(R.drawable.icon_close_small),
contentDescription = stringResource(R.string.preference_text_clear)
)
}
}
}
}
)
)
}
},
confirmButton = {
TextButton(onClick = { onSelect(enteredValue) }) {
@@ -305,34 +309,29 @@ fun RangeDialog(
mutableFloatStateOf(initialValue / scale.toFloat())
}
AlertDialog(
BaseDialog(
onDismissRequest = { onDismissRequest() },
title = {
Text(title)
},
text = {
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1.0f, false)
) {
val precision = log10(scale.toFloat()).roundToInt()
title = title,
content = {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
) {
val precision = log10(scale.toFloat()).roundToInt()
Slider(
value = enteredValue,
onValueChange = { enteredValue = it },
valueRange = (range.first.toFloat()/scale)..(range.last.toFloat()/scale),
steps = ((range.last - range.first) - 1) / step,
modifier = Modifier.weight(1.0f)
)
Text("%.${precision}f$labelSuffix".format(enteredValue))
}
children()
Slider(
value = enteredValue,
onValueChange = { enteredValue = it },
valueRange = (range.first.toFloat()/scale)..(range.last.toFloat()/scale),
steps = ((range.last - range.first) - 1) / step,
modifier = Modifier.weight(1.0f)
)
Text("%.${precision}f$labelSuffix".format(enteredValue))
}
children()
},
confirmButton = {
TextButton(onClick = { onSelect((enteredValue * scale).roundToInt()) }) {
@@ -367,16 +366,16 @@ fun FrameRateDialog(
val maximumReached = currentValue != null && currentValue > maxFrameRate
val minimumReached = currentValue != null && currentValue < minFrameRate
AlertDialog(
BaseDialog(
onDismissRequest = { onDismissRequest() },
title = {
Text(title)
},
text = {
Column {
title = title,
content = {
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1.0f, false)
) {
IconButton(onClick = {
val prevValue = max((currentValue ?: 0) - 5, minFrameRate)
@@ -416,7 +415,7 @@ fun FrameRateDialog(
enteredValue = nextValue.toString()
}, enabled = currentValue == null || currentValue < maxFrameRate) {
Icon(
Icons.Filled.Add,
painterResource(R.drawable.icon_add),
contentDescription = stringResource(R.string.preference_limit_framerate_add)
)
}
@@ -629,32 +628,30 @@ fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
val isDuplicate = currentProfiles.contains(filename)
AlertDialog(
BaseDialog(
onDismissRequest = { onDismissRequest() },
title = {
Text(stringResource(R.string.preference_profiles_create))
},
text = {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1.0f, false)
) {
OutlinedTextField(
value = enteredValue,
onValueChange = {
enteredValue = it
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
label = {
Text(stringResource(R.string.preference_profiles_create_name))
},
isError = isDuplicate
)
}
title = stringResource(R.string.preference_profiles_create),
content = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
) {
OutlinedTextField(
value = enteredValue,
onValueChange = {
enteredValue = it
},
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
),
label = {
Text(stringResource(R.string.preference_profiles_create_name))
},
isError = isDuplicate,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
if (isDuplicate) {
Spacer(Modifier.size(8.dp))
@@ -793,7 +790,7 @@ fun ProfileSelectCard() {
)
} else {
Icon(
Icons.Filled.Delete,
painterResource(R.drawable.icon_delete),
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
)
}
@@ -805,6 +802,54 @@ fun ProfileSelectCard() {
}
}
@Composable
fun BaseDialog(
title: String,
onDismissRequest: () -> Unit,
dismissButton: @Composable () -> Unit,
confirmButton: @Composable () -> Unit,
content: @Composable (ColumnScope.() -> Unit),
) {
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
content()
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
dismissButton()
confirmButton()
}
}
}
}
}
@Composable
fun <T> SelectDialog(
title: String,
@@ -815,52 +860,28 @@ fun <T> SelectDialog(
) {
val (selectedValue, setSelectedValue) = remember { mutableStateOf(initialValue) }
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
CompositionLocalProvider(
LocalSelectValue provides (selectedValue as Any),
LocalSelectSetValue provides ({
@Suppress("UNCHECKED_CAST")
setSelectedValue(it as T)
})
) {
options()
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
BaseDialog(
title = title,
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
) {
CompositionLocalProvider(
LocalSelectValue provides (selectedValue as Any),
LocalSelectSetValue provides ({
@Suppress("UNCHECKED_CAST")
setSelectedValue(it as T)
})
) {
options()
}
}
}
@@ -904,7 +925,7 @@ fun OptionsLabel(title: String, description: String? = null, icon: (@Composable
}
@Composable
fun SettingsCard(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, asCard: Boolean = true, preferenceKey: PreferenceUtils.Key) {
fun SettingsCard(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, asCard: Boolean = true, preferenceKey: PreferenceUtils.Key, onChange: ((Boolean) -> Unit)? = null) {
val context = LocalContext.current
val settingEnabled = remember {
mutableStateOf(getSetting(context, preferenceKey))
@@ -918,17 +939,21 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
icon = icon
)
},
modifier = if (asCard) Modifier.toggleable(
modifier = Modifier.toggleable(
value = settingEnabled.value,
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
onValueChange = {
settingEnabled.value = toggleSetting(context, preferenceKey)
onChange?.invoke(settingEnabled.value)
},
role = Role.Switch,
) else Modifier
),
backgroundColor = if (asCard)
MaterialTheme.colorScheme.surfaceContainer
else MaterialTheme.colorScheme.surfaceContainerHigh
) {
Switch(
checked = settingEnabled.value,
onCheckedChange = if (!asCard)
{ _ -> settingEnabled.value = toggleSetting(context, preferenceKey) }
else null
onCheckedChange = null
)
}
}
@@ -948,7 +973,7 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
Text(
description,
style = Typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
color = MaterialTheme.colorScheme.onSurface
)
}
}
@@ -956,14 +981,20 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
}
@Composable
fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
fun OptionsCard(
modifier: Modifier = Modifier,
wrapContent: Boolean = false,
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer,
title: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 1.dp)
.clip(RoundedCornerShape(4.dp))
.then(modifier)
.background(MaterialTheme.colorScheme.surfaceContainer)
.background(backgroundColor)
.defaultMinSize(minHeight = 64.dp)
.padding(horizontal = 16.dp),
Arrangement.SpaceBetween,
@@ -1,6 +1,5 @@
package com.geode.launcher.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -9,35 +8,20 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
enum class Theme {
@@ -149,7 +149,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
SCREEN_ZOOM_FIT,
LIMIT_FRAME_RATE,
DISABLE_USER_THEME,
SELECTED_ICON
SELECTED_ICON,
DEVELOPER_SERVER,
}
private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -200,6 +201,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
Key.DEVELOPER_SERVER -> "PreferenceDeveloperServer"
}
}
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,520Z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M382,720L154,492L211,435L382,606L749,239L806,296L382,720Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M336,680L280,624L424,480L280,337L336,281L480,425L623,281L679,337L535,480L679,624L623,680L480,536L336,680Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M320,760L320,200L760,480L320,760ZM400,480L400,480L400,480ZM400,614L610,480L400,346L400,614Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,800Q346,800 253,707Q160,614 160,480Q160,346 253,253Q346,160 480,160Q549,160 612,188.5Q675,217 720,270L720,160L800,160L800,440L520,440L520,360L688,360Q656,304 600.5,272Q545,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720Q557,720 619,676Q681,632 706,560L790,560Q762,666 676,733Q590,800 480,800Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M680,880Q630,880 595,845Q560,810 560,760Q560,754 563,732L282,568Q266,583 245,591.5Q224,600 200,600Q150,600 115,565Q80,530 80,480Q80,430 115,395Q150,360 200,360Q224,360 245,368.5Q266,377 282,392L563,228Q561,221 560.5,214.5Q560,208 560,200Q560,150 595,115Q630,80 680,80Q730,80 765,115Q800,150 800,200Q800,250 765,285Q730,320 680,320Q656,320 635,311.5Q614,303 598,288L317,452Q319,459 319.5,465.5Q320,472 320,480Q320,488 319.5,494.5Q319,501 317,508L598,672Q614,657 635,648.5Q656,640 680,640Q730,640 765,675Q800,710 800,760Q800,810 765,845Q730,880 680,880ZM680,800Q697,800 708.5,788.5Q720,777 720,760Q720,743 708.5,731.5Q697,720 680,720Q663,720 651.5,731.5Q640,743 640,760Q640,777 651.5,788.5Q663,800 680,800ZM200,520Q217,520 228.5,508.5Q240,497 240,480Q240,463 228.5,451.5Q217,440 200,440Q183,440 171.5,451.5Q160,463 160,480Q160,497 171.5,508.5Q183,520 200,520ZM680,240Q697,240 708.5,228.5Q720,217 720,200Q720,183 708.5,171.5Q697,160 680,160Q663,160 651.5,171.5Q640,183 640,200Q640,217 651.5,228.5Q663,240 680,240ZM680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760ZM200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480ZM680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500Z"/>
</vector>
+6
View File
@@ -262,6 +262,12 @@
<string name="preference_override_exceptions_description">Uses exception_symbols.txt to patch</string>
<string name="preference_use_index_update_api">Use index release API</string>
<string name="preference_development_server_name">Enable development server</string>
<string name="preference_development_server_description">Opens a public development server on port 9043.</string>
<string name="notification_channel_development_server">Development Server Service</string>
<string name="notification_channel_development_server_description">Indicates the development server is currently active.</string>
<!-- CrashLogsActivity strings -->
<string name="title_activity_crash_logs">Crash dumps</string>
<string name="application_crashes_no_dumps">No crash dumps available.</string>
+6 -6
View File
@@ -1,14 +1,14 @@
buildscript {
val composeBOM by extra("2025.09.00")
val composeBOM by extra("2025.12.00")
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.13.0" apply false
id("com.android.library") version "8.13.0" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
id("com.android.application") version "8.13.2" apply false
id("com.android.library") version "8.13.2" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.0" apply false
}
tasks.register<Delete>("clean") {
Binary file not shown.
+1 -2
View File
@@ -1,7 +1,6 @@
#Thu Apr 24 10:54:01 MST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+2 -3
View File
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.