mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-22 16:42:38 +00:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208819436a | ||
|
|
d7c9e82fc2 | ||
|
|
8448b3f2a9 | ||
|
|
3a6c731710 | ||
|
|
2dde48c8fc | ||
|
|
f52b90b259 | ||
|
|
fa69a649dd | ||
|
|
a16c76cd06 | ||
|
|
9b5d614d1a | ||
|
|
d6a6de8361 | ||
|
|
2797e52683 | ||
|
|
69b00fecf5 | ||
|
|
f0eddd1652 | ||
|
|
d7190fa928 | ||
|
|
8e8a56da2e | ||
|
|
b418e154da | ||
|
|
cd306a5b5e | ||
|
|
fb9a6d1912 | ||
|
|
e276e302ab | ||
|
|
d7c779fce3 | ||
|
|
1e0dcc91ec | ||
|
|
266055e6d5 | ||
|
|
ab8e3f83d2 | ||
|
|
a75d68e5e0 | ||
|
|
13191dc66f | ||
|
|
b7f378b947 | ||
|
|
25367d5650 | ||
|
|
aa51c201e5 |
No files matched your search
+29
-18
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
val composeBOM: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
@@ -68,8 +70,10 @@ android {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_1_8
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
@@ -87,29 +91,36 @@ android {
|
||||
}
|
||||
}
|
||||
namespace = "com.geode.launcher"
|
||||
ndkVersion = "28.1.13356709"
|
||||
ndkVersion = "29.0.14206865"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
|
||||
implementation ("androidx.core:core-ktx:1.16.0")
|
||||
implementation ("androidx.core:core-ktx:1.17.0")
|
||||
implementation ("androidx.compose.ui:ui")
|
||||
implementation ("androidx.compose.material3:material3")
|
||||
implementation ("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
||||
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
||||
implementation ("androidx.activity:activity-compose:1.10.1")
|
||||
implementation ("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation ("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation ("androidx.documentfile:documentfile:1.0.1")
|
||||
implementation ("com.squareup.okio:okio:3.11.0")
|
||||
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.8.1")
|
||||
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.33.0")
|
||||
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.33.0")
|
||||
implementation ("androidx.browser:browser:1.8.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.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.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")
|
||||
}
|
||||
Vendored
+1
@@ -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.**
|
||||
@@ -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"
|
||||
@@ -56,14 +58,21 @@
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_application_logs"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<activity
|
||||
android:name=".preferences.CrashLogsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_crash_logs"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<activity
|
||||
android:name=".preferences.ApplicationIconActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_application_icon"
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<activity
|
||||
android:name=".preferences.SettingsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".GeometryDashActivity"
|
||||
@@ -83,11 +92,49 @@
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityGeode"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_geode"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityPride"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_pride"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityTrans"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_trans"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<provider
|
||||
android:name=".UserDirectoryProvider"
|
||||
android:authorities="com.geode.launcher.user"
|
||||
@@ -111,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>
|
||||
@@ -13,7 +13,7 @@ option(USE_TULIPHOOK "enables choice of hooking library" OFF)
|
||||
if (USE_TULIPHOOK)
|
||||
FetchContent_Declare(TulipHook
|
||||
GIT_REPOSITORY https://github.com/geode-sdk/TulipHook.git
|
||||
GIT_TAG 9aa1f95091ebac18c657d57b755f59d00fe6f1f8
|
||||
GIT_TAG v3.1.4
|
||||
)
|
||||
|
||||
set(TULIP_LINK_SOURCE ON CACHE INTERNAL "")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -15,18 +15,13 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
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
|
||||
@@ -53,11 +48,13 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.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.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
@@ -93,6 +90,7 @@ class AltMainActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
|
||||
|
||||
@@ -101,7 +99,7 @@ class AltMainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
@@ -133,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,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))
|
||||
@@ -289,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))
|
||||
@@ -394,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)
|
||||
)
|
||||
}
|
||||
@@ -407,15 +405,18 @@ fun GeodeLogo(modifier: Modifier = Modifier) {
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.geode_logo),
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.geode_monochrome),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(76.dp, 76.dp)
|
||||
modifier = Modifier
|
||||
.size(64.dp, 64.dp),
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.launcher_title),
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
style = launcherTitleStyle,
|
||||
fontSize = 64.sp,
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 +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?) {
|
||||
@@ -62,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)
|
||||
@@ -76,9 +95,10 @@ class MainActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -158,12 +178,12 @@ 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),
|
||||
fontSize = 32.sp,
|
||||
stringResource(R.string.launcher_title),
|
||||
style = launcherTitleStyle,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
|
||||
@@ -197,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)
|
||||
)
|
||||
|
||||
@@ -208,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.geode.launcher.log
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.Instant
|
||||
|
||||
data class CrashDump @OptIn(ExperimentalTime::class) constructor(
|
||||
val filename: String,
|
||||
val lastModified: Instant
|
||||
)
|
||||
|
||||
class CrashViewModel(private val application: Application) : ViewModel() {
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
|
||||
|
||||
CrashViewModel(
|
||||
application = application
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _lineState = mutableStateListOf<CrashDump>()
|
||||
val lineState = _lineState
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private var failedToLoad = false
|
||||
|
||||
private var logJob: Job? = null
|
||||
|
||||
private fun getCrashList(): List<CrashDump> {
|
||||
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
||||
if (!crashDirectory.exists()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val children = crashDirectory.listFiles {
|
||||
// ignore indicator files (including old file)
|
||||
_, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid"
|
||||
} ?: return emptyList()
|
||||
|
||||
return children
|
||||
.map {
|
||||
CrashDump(
|
||||
filename = it.name,
|
||||
lastModified = Instant.fromEpochMilliseconds(it.lastModified())
|
||||
)
|
||||
}
|
||||
.sortedByDescending { it.lastModified }
|
||||
}
|
||||
|
||||
fun clearCrashes() {
|
||||
logJob?.cancel()
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val crashDirectory = LaunchUtils.getCrashDirectory(application)
|
||||
if (crashDirectory.exists()) {
|
||||
val files = crashDirectory.listFiles()
|
||||
files?.forEach {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_lineState.clear()
|
||||
}
|
||||
|
||||
private fun loadLogs() {
|
||||
_isLoading.value = true
|
||||
|
||||
logJob?.cancel()
|
||||
logJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
val lines = getCrashList()
|
||||
|
||||
_lineState.clear()
|
||||
_lineState.addAll(lines)
|
||||
|
||||
_isLoading.value = false
|
||||
|
||||
logJob = null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCrash(filename: String) {
|
||||
logJob?.cancel()
|
||||
|
||||
val crashPath = File(LaunchUtils.getCrashDirectory(application), filename)
|
||||
if (crashPath.exists()) {
|
||||
crashPath.delete()
|
||||
loadLogs()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
loadLogs()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.geode.launcher.log
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toJavaInstant
|
||||
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
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
fun BufferedSource.readCChar(): Char {
|
||||
return this.readUtf8CodePoint().toChar()
|
||||
@@ -25,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,
|
||||
@@ -115,6 +138,7 @@ enum class LogPriority {
|
||||
/**
|
||||
* Represents a log entry from logcat.
|
||||
*/
|
||||
@Serializable
|
||||
data class LogLine(
|
||||
val process: ProcessInformation,
|
||||
val time: Instant,
|
||||
@@ -134,6 +158,7 @@ data class LogLine(
|
||||
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun fromBufferedSource(source: BufferedSource): LogLine {
|
||||
/*
|
||||
// from android <liblog/include/log/log_read.h>
|
||||
@@ -196,7 +221,7 @@ data class LogLine(
|
||||
time = time,
|
||||
logId = lid,
|
||||
tag = tag,
|
||||
message = message
|
||||
message = message.trimEnd('\u0000')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,6 +235,7 @@ data class LogLine(
|
||||
)
|
||||
}
|
||||
|
||||
@Transient
|
||||
val identifier = time.toJavaInstant()
|
||||
|
||||
val messageTrimmed by lazy {
|
||||
@@ -217,6 +243,7 @@ data class LogLine(
|
||||
}
|
||||
|
||||
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,20 +239,25 @@ 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))
|
||||
Text(stringResource(R.string.launcher_error_export_crash))
|
||||
}
|
||||
|
||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
val developerSettingsEnabled = remember {
|
||||
PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
}
|
||||
if (developerSettingsEnabled) {
|
||||
FilledTonalButton(onClick = { onShowLogs(context) }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(stringResource(R.string.launcher_error_view_logs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -46,6 +44,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -59,13 +58,14 @@ import com.geode.launcher.utils.PreferenceUtils
|
||||
import com.mikepenz.markdown.compose.LocalBulletListHandler
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import com.mikepenz.markdown.m3.markdownTypography
|
||||
import kotlinx.datetime.toJavaInstant
|
||||
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
|
||||
import java.io.File
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
fun clearDownloadedApks(context: Context) {
|
||||
// technically we should be using the activity results but it was too inconsistent for my liking
|
||||
@@ -135,7 +135,7 @@ fun installLauncherUpdate(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
|
||||
@Composable
|
||||
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
@@ -196,9 +196,11 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
Markdown(
|
||||
content = nextRelease.release.body.replace("\r", ""),
|
||||
typography = markdownTypography(
|
||||
link = MaterialTheme.typography.bodyLarge.copy(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textLink = TextLinkStyles(
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
).toSpanStyle()
|
||||
),
|
||||
),
|
||||
flavour = GFMFlavourDescriptor(),
|
||||
@@ -316,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))
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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
|
||||
import com.geode.launcher.R
|
||||
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.utils.PreferenceUtils
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.geode.launcher.utils.ApplicationIcon
|
||||
import com.geode.launcher.utils.ApplicationIconDetails
|
||||
import com.geode.launcher.utils.IconUtils
|
||||
import com.geode.launcher.utils.adaptiveIconPainterResource
|
||||
|
||||
class ApplicationIconActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ApplicationIconScreen(onBackPressedDispatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
try {
|
||||
ApplicationIcon.entries.forEach {
|
||||
val iconData = IconUtils.getIconDetails(it)
|
||||
|
||||
packageManager.setComponentEnabledSetting(
|
||||
ComponentName(context, "${context.packageName}.${iconData.component}"),
|
||||
if (iconData.id == selectedIcon.id)
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
Toast.makeText(context, R.string.application_icon_set_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
PreferenceUtils.get(context).setString(PreferenceUtils.Key.SELECTED_ICON, selectedIcon.id)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconCard(iconData: ApplicationIconDetails, selected: Boolean, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.selectable(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
onSelectIcon(context, iconData)
|
||||
},
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
Arrangement.SpaceBetween,
|
||||
Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
adaptiveIconPainterResource(iconData.iconId),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(iconData.nameId))
|
||||
}
|
||||
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ApplicationIconScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
var showMoreOptions by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_arrow_back),
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.title_activity_application_icon),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
||||
|
||||
Column(Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
ApplicationIcon.entries.forEach {
|
||||
val iconData = IconUtils.getIconDetails(it)
|
||||
|
||||
IconCard(iconData, selectedIcon == iconData.id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -70,6 +66,7 @@ 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.Typography
|
||||
import com.geode.launcher.ui.theme.robotoMonoFamily
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -83,9 +80,10 @@ class ApplicationLogsActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -166,12 +164,12 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
|
||||
style = Typography.labelSmall,
|
||||
color = getLogColor(logLine.priority),
|
||||
fontFamily = FontFamily.Monospace
|
||||
fontFamily = robotoMonoFamily
|
||||
)
|
||||
|
||||
Text(
|
||||
"[${logLine.tag}]: ${logLine.messageTrimmed}",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontFamily = robotoMonoFamily,
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
}
|
||||
@@ -187,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()
|
||||
@@ -234,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)
|
||||
)
|
||||
}
|
||||
@@ -242,26 +241,17 @@ fun ApplicationLogsScreen(
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
val data = logViewModel.getLogData()
|
||||
|
||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, data)
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
|
||||
context.startActivity(shareIntent)
|
||||
saveLauncher.launch(resources.getString(R.string.application_logs_default_filename))
|
||||
}
|
||||
}, enabled = !logLines.isEmpty()) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = stringResource(R.string.application_logs_share)
|
||||
painterResource(R.drawable.icon_save),
|
||||
contentDescription = stringResource(R.string.application_logs_export),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||
Icon(
|
||||
Icons.Filled.MoreVert,
|
||||
painterResource(R.drawable.icon_more_vert),
|
||||
contentDescription = stringResource(R.string.application_logs_more)
|
||||
)
|
||||
}
|
||||
@@ -280,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)
|
||||
)
|
||||
}
|
||||
@@ -309,23 +299,6 @@ fun ApplicationLogsScreen(
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_save),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_export))
|
||||
},
|
||||
onClick = {
|
||||
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
||||
showMoreOptions = false
|
||||
},
|
||||
enabled = !logLines.isEmpty()
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
import android.widget.Toast
|
||||
import java.text.DateFormat
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
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.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.UserDirectoryProvider
|
||||
import com.geode.launcher.log.CrashDump
|
||||
import com.geode.launcher.log.CrashViewModel
|
||||
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.Typography
|
||||
import com.geode.launcher.ui.theme.robotoMonoFamily
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
class CrashLogsActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
CrashLogsScreen(onBackPressedDispatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shareCrash(context: Context, filename: String) {
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
val crashPath = File(LaunchUtils.getCrashDirectory(context), filename)
|
||||
|
||||
if (!crashPath.exists()) {
|
||||
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val providerLocation = crashPath.toRelativeString(baseDirectory)
|
||||
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
|
||||
|
||||
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
|
||||
try {
|
||||
context.startActivity(shareIntent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp, horizontal = 8.dp)
|
||||
.height(64.dp)
|
||||
.then(modifier)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
modifier = Modifier.fillMaxHeight().offset(x = 12.dp).weight(0.7f)
|
||||
) {
|
||||
Text(
|
||||
crashDump.filename,
|
||||
fontFamily = robotoMonoFamily,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
val formattedLastModified = remember {
|
||||
DateFormat.getDateTimeInstance()
|
||||
.format(
|
||||
Date.from(crashDump.lastModified.toJavaInstant())
|
||||
)
|
||||
|
||||
}
|
||||
Text(formattedLastModified, style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.weight(0.3f), horizontalArrangement = Arrangement.End) {
|
||||
val context = LocalContext.current
|
||||
IconButton(onClick = {
|
||||
shareCrash(context, crashDump.filename)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_share),
|
||||
contentDescription = stringResource(R.string.application_logs_share)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
crashViewModel.removeCrash(crashDump.filename)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = stringResource(R.string.application_crashes_delete_one)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CrashLogsScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
crashViewModel: CrashViewModel = viewModel(factory = CrashViewModel.Factory)
|
||||
) {
|
||||
val logLines = crashViewModel.lineState
|
||||
val isLoading by crashViewModel.isLoading.collectAsState()
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
var showMoreOptions by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_arrow_back),
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_more_vert),
|
||||
contentDescription = stringResource(R.string.application_logs_more)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMoreOptions,
|
||||
onDismissRequest = { showMoreOptions = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_delete))
|
||||
}, onClick = {
|
||||
crashViewModel.clearCrashes()
|
||||
showMoreOptions = false
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.title_activity_crash_logs),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
if (!isLoading && logLines.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.application_crashes_no_dumps),
|
||||
textAlign = TextAlign.Center,
|
||||
style = Typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxSize().padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val itemsSize = logLines.size
|
||||
|
||||
itemsIndexed(
|
||||
logLines,
|
||||
key = { index, line -> line.filename }
|
||||
) { index, line ->
|
||||
val shape = RoundedCornerShape(
|
||||
topStart = if (index == 0) 16.dp else 4.dp,
|
||||
topEnd = if (index == 0) 16.dp else 4.dp,
|
||||
bottomStart = if (index + 1 == itemsSize) 16.dp else 4.dp,
|
||||
bottomEnd = if (index + 1 == itemsSize) 16.dp else 4.dp
|
||||
)
|
||||
|
||||
CrashCard(
|
||||
line,
|
||||
crashViewModel,
|
||||
modifier = Modifier.clip(shape)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun CrashLogsPreview() {
|
||||
GeodeLauncherTheme {
|
||||
CrashLogsScreen(null)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,25 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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
|
||||
@@ -32,14 +36,17 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.geode.launcher.R
|
||||
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.utils.LabelledText
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
|
||||
class DeveloperSettingsActivity : ComponentActivity() {
|
||||
@@ -52,9 +59,10 @@ class DeveloperSettingsActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -67,6 +75,30 @@ class DeveloperSettingsActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenFolder(context: Context) {
|
||||
val file = LaunchUtils.getBaseDirectory(context)
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
context.getString(R.string.export_folder_tag),
|
||||
file.path
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.text_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
@@ -80,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)
|
||||
)
|
||||
}
|
||||
@@ -93,6 +125,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,16 +140,12 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
|
||||
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_developer_mode),
|
||||
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
|
||||
)
|
||||
|
||||
InlineText(
|
||||
stringResource(R.string.preference_developer_mode_about)
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
OptionsGroup {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_developer_mode),
|
||||
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
|
||||
)
|
||||
}
|
||||
|
||||
if (developerMode) {
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_gameplay)) {
|
||||
@@ -130,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),
|
||||
@@ -145,12 +175,39 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
*/
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_use_index_update_api),
|
||||
preferenceKey = PreferenceUtils.Key.USE_INDEX_API
|
||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||
val context = LocalContext.current
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_logs),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_description),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenLogs(context) }
|
||||
)
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_copy_external_button),
|
||||
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)) {
|
||||
SettingsSelectCard(
|
||||
title = stringResource(R.string.preference_release_channel_tag_name),
|
||||
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
||||
@@ -161,16 +218,6 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
2 to stringResource(R.string.preference_release_channel_nightly),
|
||||
)
|
||||
)
|
||||
|
||||
ElevatedCard(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
|
||||
LabelledText(
|
||||
label = stringResource(R.string.preference_developer_update_notice),
|
||||
icon = {
|
||||
Icon(Icons.Filled.Info, null)
|
||||
},
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
|
||||
@@ -185,6 +232,8 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
ProfileSelectCard()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package com.geode.launcher.preferences
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@@ -23,25 +21,23 @@ 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.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
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
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -53,8 +49,10 @@ 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.utils.ApplicationIcon
|
||||
import com.geode.launcher.utils.Constants
|
||||
import com.geode.launcher.utils.GamePackageUtils
|
||||
import com.geode.launcher.utils.IconUtils
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import java.net.ConnectException
|
||||
@@ -71,9 +69,10 @@ class SettingsActivity : ComponentActivity() {
|
||||
val theme = Theme.fromInt(themeOption)
|
||||
|
||||
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
|
||||
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -88,25 +87,6 @@ class SettingsActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenFolder(context: Context) {
|
||||
val file = LaunchUtils.getBaseDirectory(context)
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
context.getString(R.string.export_folder_tag),
|
||||
file.path
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.text_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenFileManager(context: Context) {
|
||||
// future thoughts: Intent.ACTION_VIEW may not work on some devices
|
||||
// the "correct" solution is to go through a list of possible intents/applications
|
||||
@@ -141,6 +121,7 @@ fun UpdateIndicator(
|
||||
updateStatus: ReleaseViewModel.ReleaseUIState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
|
||||
var enablePopup by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -176,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)
|
||||
@@ -185,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -226,16 +207,36 @@ fun updateTheme(context: Context, theme: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
fun onOpenDeveloperOptions(context: Context) {
|
||||
val launchIntent = Intent(context, DeveloperSettingsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
|
||||
AlertDialog(
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_warning),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.preference_developer_options_dialog_title)) },
|
||||
text = { Text(stringResource(R.string.preference_developer_mode_about)) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onEnable) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
@@ -243,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()
|
||||
@@ -253,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
|
||||
}
|
||||
@@ -278,19 +280,20 @@ 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
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
@@ -301,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),
|
||||
@@ -317,16 +320,49 @@ fun SettingsScreen(
|
||||
title = stringResource(R.string.preference_black_background_name),
|
||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SettingsCard(
|
||||
title = stringResource(R.string.preference_disable_styles),
|
||||
preferenceKey = PreferenceUtils.Key.DISABLE_USER_THEME
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val currentIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
|
||||
val iconName = remember(currentIcon) {
|
||||
IconUtils.getIconDetails(
|
||||
ApplicationIcon.fromId(currentIcon ?: "default")
|
||||
).nameId
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_set_application_icon),
|
||||
onClick = {
|
||||
val launchIntent = Intent(context, ApplicationIconActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
},
|
||||
description = stringResource(iconName),
|
||||
displayInline = true,
|
||||
)
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_open_file_manager),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_folder),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenFileManager(context) }
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -371,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
|
||||
)
|
||||
@@ -394,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(
|
||||
@@ -425,52 +461,72 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val developerModeEnabled by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||
if (developerModeEnabled) {
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_open_developer_options),
|
||||
icon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_data_object),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenDeveloperOptions(context) }
|
||||
)
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_logs),
|
||||
title = stringResource(R.string.preferences_view_crashes),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_description), contentDescription = null)
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_bug_report),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
onClick = { onOpenLogs(context) }
|
||||
)
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preference_open_developer_options),
|
||||
icon = {
|
||||
Icon(painterResource(R.drawable.icon_data_object), contentDescription = null)
|
||||
},
|
||||
onClick = { onOpenDeveloperOptions(context) }
|
||||
)
|
||||
OptionsButton(
|
||||
title = context.getString(R.string.preferences_copy_external_button),
|
||||
description = LaunchUtils.getBaseDirectory(context).path,
|
||||
onClick = { onOpenFolder(context) }
|
||||
onClick = {
|
||||
val launchIntent = Intent(context, CrashLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_about)) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
var launcherButtonTapped by remember { mutableIntStateOf(0) }
|
||||
var showDeveloperDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsButton(
|
||||
stringResource(R.string.preference_launcher_version_name),
|
||||
stringResource(R.string.preference_launcher_version_description, BuildConfig.VERSION_NAME),
|
||||
displayInline = true
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_launcher_version,
|
||||
BuildConfig.VERSION_NAME
|
||||
)))
|
||||
launcherButtonTapped += 1
|
||||
|
||||
if (launcherButtonTapped >= 7 && !developerModeEnabled) {
|
||||
showDeveloperDialog = true
|
||||
launcherButtonTapped = 0
|
||||
}
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
val context = LocalContext.current
|
||||
if (showDeveloperDialog) {
|
||||
DeveloperModeDialog(
|
||||
onDismiss = {
|
||||
showDeveloperDialog = false
|
||||
},
|
||||
onEnable = {
|
||||
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.DEVELOPER_MODE, true)
|
||||
showDeveloperDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_loader_version_name),
|
||||
stringResource(R.string.preference_loader_version_description, currentRelease ?: "unknown"),
|
||||
displayInline = true
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_loader_version,
|
||||
currentRelease ?: "unknown"
|
||||
)))
|
||||
}
|
||||
)
|
||||
|
||||
val gameInstalled = remember {
|
||||
GamePackageUtils.isGameInstalled(context.packageManager)
|
||||
@@ -482,38 +538,28 @@ 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
|
||||
})
|
||||
}
|
||||
|
||||
OptionsButton(
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_game_version_name),
|
||||
stringResource(R.string.preference_game_version_description, gameVersion, gameSource),
|
||||
displayInline = true
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_game_version,
|
||||
gameVersion,
|
||||
gameSource
|
||||
)))
|
||||
}
|
||||
)
|
||||
|
||||
OptionsButton(
|
||||
OptionsLabel(
|
||||
stringResource(R.string.preference_architecture_name),
|
||||
stringResource(R.string.preference_architecture_description, Build.MODEL, LaunchUtils.applicationArchitecture),
|
||||
displayInline = true
|
||||
) {
|
||||
clipboard.setText(AnnotatedString(context.getString(
|
||||
R.string.preference_architecture,
|
||||
Build.MODEL,
|
||||
LaunchUtils.applicationArchitecture
|
||||
)))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,10 +3,12 @@ package com.geode.launcher.preferences
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.background
|
||||
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
|
||||
@@ -18,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
|
||||
@@ -46,6 +44,8 @@ import androidx.compose.runtime.setValue
|
||||
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
|
||||
@@ -82,14 +82,22 @@ fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
||||
fun OptionsGroup(title: String? = null, content: @Composable () -> Unit) {
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
content()
|
||||
if (title != null) {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,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) }) {
|
||||
@@ -295,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()) }) {
|
||||
@@ -357,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)
|
||||
@@ -406,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)
|
||||
)
|
||||
}
|
||||
@@ -619,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))
|
||||
@@ -783,7 +790,7 @@ fun ProfileSelectCard() {
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
|
||||
)
|
||||
}
|
||||
@@ -795,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,
|
||||
@@ -805,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,7 +907,25 @@ fun OptionsButton(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 OptionsLabel(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, displayInline: Boolean = false) {
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
title = title,
|
||||
description = description.takeIf { !displayInline },
|
||||
icon = icon
|
||||
)
|
||||
},
|
||||
wrapContent = true
|
||||
) {
|
||||
if (displayInline && description != null) {
|
||||
Text(description, textAlign = TextAlign.End)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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))
|
||||
@@ -890,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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -920,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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -928,10 +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
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(modifier)
|
||||
.background(backgroundColor)
|
||||
.defaultMinSize(minHeight = 64.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
Arrangement.SpaceBetween,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.geode.launcher.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.geode.launcher.R
|
||||
|
||||
val robotoMonoFamily = FontFamily(
|
||||
Font(R.font.robotomono_medium, FontWeight.Medium),
|
||||
Font(R.font.robotomono_regular, FontWeight.Normal)
|
||||
)
|
||||
|
||||
val quicksandFamily = FontFamily(
|
||||
Font(R.font.quicksand_semibold, FontWeight.SemiBold)
|
||||
)
|
||||
|
||||
val launcherTitleStyle = TextStyle(
|
||||
fontFamily = quicksandFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.geode.launcher.updater
|
||||
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@Serializable
|
||||
data class Asset(
|
||||
data class Asset @OptIn(ExperimentalTime::class) constructor(
|
||||
val url: String,
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@@ -16,7 +17,7 @@ data class Asset(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Release(
|
||||
data class Release @OptIn(ExperimentalTime::class) constructor(
|
||||
val url: String,
|
||||
val id: Int,
|
||||
val targetCommitish: String,
|
||||
@@ -30,7 +31,7 @@ data class Release(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoaderVersion(
|
||||
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
|
||||
val tag: String,
|
||||
val version: String,
|
||||
val createdAt: Instant,
|
||||
@@ -66,6 +67,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
@@ -95,6 +97,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
||||
return release.tagName
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return release.createdAt.epochSeconds
|
||||
}
|
||||
@@ -132,6 +135,7 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
|
||||
return version.tag
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override fun getDescriptor(): Long {
|
||||
return version.createdAt.epochSeconds
|
||||
}
|
||||
|
||||
@@ -122,22 +122,12 @@ class ReleaseManager private constructor(
|
||||
val gameVersion = GamePackageUtils.getGameVersionCode(applicationContext.packageManager)
|
||||
val preferences = PreferenceUtils.get(applicationContext)
|
||||
|
||||
if (preferences.getBoolean(PreferenceUtils.Key.USE_INDEX_API)) {
|
||||
val targetTag = preferences.getInt(PreferenceUtils.Key.RELEASE_CHANNEL_TAG)
|
||||
val targetTag = preferences.getInt(PreferenceUtils.Key.RELEASE_CHANNEL_TAG)
|
||||
|
||||
return when (targetTag) {
|
||||
2 -> releaseRepository.getReleaseByTag("nightly")
|
||||
1 -> releaseRepository.getLatestIndexRelease(gameVersion, true)
|
||||
else -> releaseRepository.getLatestIndexRelease(gameVersion, false)
|
||||
}
|
||||
} else {
|
||||
val targetTag = getBestReleaseForGameVersion(gameVersion) ?: return null
|
||||
|
||||
return when (targetTag) {
|
||||
TAG_LATEST -> releaseRepository.getLatestGeodeRelease()
|
||||
TAG_BETA -> releaseRepository.getLatestGeodePreRelease()
|
||||
else -> releaseRepository.getReleaseByTag(targetTag)
|
||||
}
|
||||
return when (targetTag) {
|
||||
2 -> releaseRepository.getReleaseByTag("nightly")
|
||||
1 -> releaseRepository.getLatestIndexRelease(gameVersion, true)
|
||||
else -> releaseRepository.getLatestIndexRelease(gameVersion, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.geode.launcher.updater
|
||||
|
||||
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.datetime.until
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -14,6 +14,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
companion object {
|
||||
@@ -49,7 +50,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
|
||||
suspend fun getLatestGeodePreRelease(): DownloadableGitHubLoaderRelease? {
|
||||
val releasesUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases?per_page=2"
|
||||
val url = URL(releasesUrl)
|
||||
@@ -104,6 +105,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
private fun generateRateLimitMessage(resetTime: Instant): String {
|
||||
val currentTime = Clock.System.now()
|
||||
val resetDelay = currentTime.until(resetTime, DateTimeUnit.SECOND)
|
||||
@@ -113,7 +115,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
|
||||
return "api ratelimit reached, try again in ${formattedWait}"
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
|
||||
private suspend fun getReleaseByUrl(url: URL): Release? {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.geode.launcher.UserDirectoryProvider
|
||||
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
|
||||
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
|
||||
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
|
||||
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
@@ -477,6 +476,10 @@ object GeodeUtils {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun vibrate(ms: Long) {
|
||||
if (ms == 0L) {
|
||||
getVibrator()?.cancel()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getVibrator()?.vibrate(VibrationEffect.createOneShot(ms, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
@@ -573,11 +576,6 @@ object GeodeUtils {
|
||||
.launchUrl(activity, url.toUri())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun openMultilineIMEKeyboard() {
|
||||
Cocos2dxGLSurfaceView.openMultilineIMEKeyboard()
|
||||
}
|
||||
|
||||
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
|
||||
external fun nativeKeyDown(keyCode: Int, modifiers: Int, isRepeating: Boolean)
|
||||
external fun nativeActionScroll(scrollX: Float, scrollY: Float)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.os.Build
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.geode.launcher.R
|
||||
|
||||
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
|
||||
@Composable
|
||||
fun adaptiveIconPainterResource(@DrawableRes id: Int): Painter {
|
||||
val context = LocalContext.current
|
||||
val res = LocalResources.current
|
||||
val theme = context.theme
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val adaptiveIcon = remember(id) {
|
||||
ResourcesCompat.getDrawable(res, id, theme) as? AdaptiveIconDrawable
|
||||
}
|
||||
if (adaptiveIcon != null) {
|
||||
remember(id) {
|
||||
BitmapPainter(adaptiveIcon.toBitmap().asImageBitmap())
|
||||
}
|
||||
} else {
|
||||
painterResource(id)
|
||||
}
|
||||
} else {
|
||||
painterResource(id)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ApplicationIcon {
|
||||
DEFAULT,
|
||||
GEODE,
|
||||
PRIDE,
|
||||
TRANS;
|
||||
|
||||
companion object {
|
||||
fun fromId(id: String) = when (id) {
|
||||
"default" -> ApplicationIcon.DEFAULT
|
||||
"geode" -> ApplicationIcon.GEODE
|
||||
"pride" -> ApplicationIcon.PRIDE
|
||||
"trans" -> ApplicationIcon.TRANS
|
||||
else -> ApplicationIcon.DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
fun toId() = when (this) {
|
||||
ApplicationIcon.PRIDE -> "pride"
|
||||
ApplicationIcon.GEODE -> "geode"
|
||||
ApplicationIcon.DEFAULT -> "default"
|
||||
ApplicationIcon.TRANS -> "trans"
|
||||
}
|
||||
}
|
||||
|
||||
data class ApplicationIconDetails(
|
||||
val id: String,
|
||||
val component: String,
|
||||
@param:DrawableRes val iconId: Int,
|
||||
@param:StringRes val nameId: Int,
|
||||
)
|
||||
|
||||
object IconUtils {
|
||||
fun getIconDetails(id: ApplicationIcon) = when (id) {
|
||||
ApplicationIcon.DEFAULT -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivity",
|
||||
iconId = R.mipmap.ic_launcher,
|
||||
nameId = R.string.application_icon_default
|
||||
)
|
||||
ApplicationIcon.GEODE -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivityGeode",
|
||||
iconId = R.mipmap.ic_launcher_geode,
|
||||
nameId = R.string.application_icon_geode
|
||||
)
|
||||
ApplicationIcon.PRIDE -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivityPride",
|
||||
iconId = R.mipmap.ic_launcher_pride,
|
||||
nameId = R.string.application_icon_pride
|
||||
)
|
||||
ApplicationIcon.TRANS -> ApplicationIconDetails(
|
||||
id = id.toId(),
|
||||
component = "MainActivityTrans",
|
||||
iconId = R.mipmap.ic_launcher_trans,
|
||||
nameId = R.string.application_icon_trans
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,9 +73,9 @@ object LaunchUtils {
|
||||
return File(getBaseDirectory(context), "save")
|
||||
}
|
||||
|
||||
private const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
|
||||
const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
|
||||
|
||||
private fun getCrashDirectory(context: Context): File {
|
||||
fun getCrashDirectory(context: Context): File {
|
||||
val base = getBaseDirectory(context)
|
||||
return File(base, "game/geode/crashlogs/")
|
||||
}
|
||||
|
||||
@@ -148,14 +148,15 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
SCREEN_ZOOM,
|
||||
SCREEN_ZOOM_FIT,
|
||||
LIMIT_FRAME_RATE,
|
||||
USE_INDEX_API,
|
||||
DISABLE_USER_THEME,
|
||||
SELECTED_ICON,
|
||||
DEVELOPER_SERVER,
|
||||
}
|
||||
|
||||
private fun defaultValueForBooleanKey(key: Key): Boolean {
|
||||
return when (key) {
|
||||
Key.UPDATE_AUTOMATICALLY, Key.FORCE_HRR -> true
|
||||
Key.DEVELOPER_MODE -> BuildConfig.DEBUG
|
||||
Key.USE_INDEX_API -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -169,6 +170,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
|
||||
private fun defaultValueForStringKey(key: Key) = when (key) {
|
||||
Key.CUSTOM_ASPECT_RATIO -> "16_9"
|
||||
Key.SELECTED_ICON -> "default"
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -197,7 +199,9 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
|
||||
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
|
||||
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
|
||||
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
|
||||
Key.USE_INDEX_API -> "PreferenceUseIndexApi"
|
||||
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
|
||||
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
|
||||
Key.DEVELOPER_SERVER -> "PreferenceDeveloperServer"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
@@ -20,8 +19,6 @@ import com.geode.launcher.utils.GeodeUtils
|
||||
|
||||
private const val HANDLER_OPEN_IME_KEYBOARD = 2
|
||||
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
|
||||
private const val HANDLER_ARG_MULTILINE = 1
|
||||
private const val HANDLER_ARG_SINGLELINE = 0
|
||||
private const val MS_TO_NS = 1_000_000
|
||||
|
||||
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
@@ -30,22 +27,11 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
private lateinit var handler: Handler
|
||||
private lateinit var cocos2dxTextInputWrapper: Cocos2dxTextInputWrapper
|
||||
|
||||
fun openMultilineIMEKeyboard() {
|
||||
val msg = Message().apply {
|
||||
what = HANDLER_OPEN_IME_KEYBOARD
|
||||
obj = cocos2dxGLSurfaceView.getContentText()
|
||||
arg1 = HANDLER_ARG_MULTILINE
|
||||
}
|
||||
handler.sendMessage(msg)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun openIMEKeyboard() {
|
||||
val msg = Message().apply {
|
||||
what = HANDLER_OPEN_IME_KEYBOARD
|
||||
obj = cocos2dxGLSurfaceView.getContentText()
|
||||
arg1 = HANDLER_ARG_SINGLELINE
|
||||
}
|
||||
val msg = Message()
|
||||
msg.what = HANDLER_OPEN_IME_KEYBOARD
|
||||
msg.obj = cocos2dxGLSurfaceView.getContentText()
|
||||
handler.sendMessage(msg)
|
||||
}
|
||||
|
||||
@@ -81,18 +67,6 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
requestFocus()
|
||||
}
|
||||
|
||||
fun setKeyboardMultiline() {
|
||||
val editText = cocos2dxEditText ?: return
|
||||
editText.inputType = editText.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
editText.maxLines = Int.MAX_VALUE
|
||||
}
|
||||
|
||||
fun setKeyboardSingleLine() {
|
||||
val editText = cocos2dxEditText ?: return
|
||||
editText.inputType = editText.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE.inv()
|
||||
editText.maxLines = 1
|
||||
}
|
||||
|
||||
fun initView() {
|
||||
setEGLContextClientVersion(2)
|
||||
isFocusableInTouchMode = true
|
||||
@@ -104,12 +78,6 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
|
||||
when (msg.what) {
|
||||
HANDLER_OPEN_IME_KEYBOARD -> {
|
||||
if (cocos2dxEditText?.requestFocus() == true) {
|
||||
if (msg.arg1 == HANDLER_ARG_MULTILINE) {
|
||||
setKeyboardMultiline()
|
||||
} else {
|
||||
setKeyboardSingleLine()
|
||||
}
|
||||
|
||||
cocos2dxEditText?.apply {
|
||||
removeTextChangedListener(
|
||||
cocos2dxTextInputWrapper
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.text.TextWatcher
|
||||
import android.widget.TextView.OnEditorActionListener
|
||||
import android.text.Editable
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
|
||||
@@ -48,10 +47,6 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
|
||||
|
||||
override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean {
|
||||
if (actionID != EditorInfo.IME_ACTION_DONE) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit) {
|
||||
for (i in originText!!.length downTo 1) {
|
||||
cocos2dxGLSurfaceView.deleteBackward()
|
||||
@@ -63,7 +58,9 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
|
||||
|
||||
cocos2dxGLSurfaceView.insertText(text)
|
||||
}
|
||||
|
||||
if (actionID != 6) {
|
||||
return false
|
||||
}
|
||||
cocos2dxGLSurfaceView.requestFocus()
|
||||
Cocos2dxGLSurfaceView.closeIMEKeyboard()
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.6689189"
|
||||
android:scaleY="0.6689189"
|
||||
android:translateX="169.51352"
|
||||
android:translateY="182.1982">
|
||||
<path
|
||||
android:pathData="M553.7,189.9L536.9,177.1C522.2,165.8 501.8,165.8 487.1,177.1L207.6,390.5C194,400.9 188.3,418.8 193.3,435.2L300.9,783.2C306.2,800.3 322.1,812.1 340.1,812.1H683.9C701.9,812.1 717.8,800.3 723.1,783.2L830.7,435.2C835.7,418.8 830,400.9 816.4,390.5L792,371.9L762,466.3L680,733C676.5,744.4 666,752.1 654.2,752.1H368C356.1,752.1 345.7,744.4 342.2,733L252.8,442.3C249.5,431.5 253.2,419.8 262.1,413L494.6,234.5C495.6,233.7 496.6,233.1 497.6,232.5L553.7,189.9Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.25"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M500.6,320.6C508.5,314.5 519.5,314.5 527.4,320.6L686,442.2C693.3,447.8 696.3,457.3 693.6,466.1L632.6,664.4C629.7,673.6 621.2,679.9 611.6,679.9H416.4C406.8,679.9 398.3,673.6 395.4,664.4L385.4,632H439.4C440,632.1 440.7,632.2 441.4,632.2H583.2C588,632.2 592.3,629 593.7,624.4L638,477.5C639.3,473.2 637.8,468.5 634.3,465.7L519.1,375.6C515.1,372.5 509.5,372.5 505.5,375.6L399.1,458.8L344.5,499.1L334.4,466.1C331.7,457.3 334.7,447.8 342,442.2L500.6,320.6Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.25"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M557.1,165.4L536.9,150C522.2,138.8 501.8,138.8 487.1,150L180.9,383.8C167.3,394.2 161.6,412.1 166.7,428.5L284.4,809.4C289.7,826.6 305.6,838.3 323.6,838.3H700.4C718.4,838.3 734.3,826.6 739.6,809.4L857.3,428.5C862.4,412.1 856.7,394.2 843.1,383.8L814.8,362.3L782.4,464.4L693.2,754.4C689.7,765.7 679.2,773.5 667.4,773.5H354.6C342.8,773.5 332.3,765.7 328.8,754.4L231.2,437C227.9,426.2 231.6,414.5 240.6,407.6L494.6,212.6C495.6,211.8 496.6,211.1 497.7,210.5L557.1,165.4Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#ffffff" />
|
||||
<path
|
||||
android:pathData="M500.6,284.3C508.5,278.2 519.5,278.2 527.4,284.3L716.8,429.5C724.1,435.1 727.1,444.6 724.4,453.4L651.6,689.9C648.8,699.2 640.3,705.5 630.6,705.5H397.4C387.7,705.5 379.2,699.2 376.4,689.9L363.9,649.5H425.7C426.4,649.6 427.1,649.7 427.9,649.7H596.1C601,649.7 605.2,646.5 606.6,641.9L659.1,468C660.5,463.6 659,458.9 655.4,456.1L518.8,349.3C514.8,346.2 509.2,346.2 505.2,349.3L379.9,447.3L316.2,494.3L303.6,453.4C300.9,444.6 303.9,435.1 311.2,429.5L500.6,284.3Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#ffffff" />
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||
<clip-path
|
||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||
<path
|
||||
android:pathData="M0,0h1024v1024h-1024z"
|
||||
android:fillColor="#F4D48E"/>
|
||||
<path
|
||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||
android:fillColor="#F4D48E"/>
|
||||
<path
|
||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||
android:fillColor="#F4A47B"/>
|
||||
<path
|
||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||
android:fillColor="#E27880"/>
|
||||
<path
|
||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||
android:fillColor="#B9588F"/>
|
||||
<path
|
||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||
android:fillColor="#714A9A"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||
<clip-path
|
||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||
<path
|
||||
android:pathData="M0,0h1024v1024h-1024z"
|
||||
android:fillColor="#CC7C7C"/>
|
||||
<path
|
||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||
android:fillColor="#CC7C7C"/>
|
||||
<path
|
||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||
android:fillColor="#C6B471"/>
|
||||
<path
|
||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||
android:fillColor="#59AF7F"/>
|
||||
<path
|
||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||
android:fillColor="#5F8CAB"/>
|
||||
<path
|
||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||
android:fillColor="#70558D"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
|
||||
<clip-path
|
||||
android:pathData="M0,0h1024v1024h-1024z"/>
|
||||
<path
|
||||
android:pathData="M0,0h1024v1024h-1024z"
|
||||
android:fillColor="#65B8C2"/>
|
||||
<path
|
||||
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
|
||||
android:fillColor="#65B8C2"/>
|
||||
<path
|
||||
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
|
||||
android:fillColor="#DEA5CD"/>
|
||||
<path
|
||||
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
|
||||
android:fillColor="#DEE8EA"/>
|
||||
<path
|
||||
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
|
||||
android:fillColor="#C485BD"/>
|
||||
<path
|
||||
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
|
||||
android:fillColor="#259CB4"/>
|
||||
</group>
|
||||
</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="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>
|
||||
@@ -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>
|
||||
@@ -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="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,320Q800,320 800,320Q800,320 800,320L447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240L160,320L160,320Q160,320 160,320Q160,320 160,320L160,720Q160,720 160,720Q160,720 160,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="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>
|
||||
@@ -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>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_geode_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_pride_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_trans_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
|
||||
</adaptive-icon>
|
||||
@@ -12,7 +12,7 @@
|
||||
<string name="launcher_logo_alt">geode logo</string>
|
||||
<string name="launcher_launch_icon_alt">play icon</string>
|
||||
<string name="launcher_download_icon_alt">download icon</string>
|
||||
<string name="launcher_download_game">Download Game</string>
|
||||
<string name="launcher_download_game">Download game</string>
|
||||
<string name="launcher_download_32bit">Download the 32-bit version of launcher.</string>
|
||||
<string name="launcher_download_universal">Download the non 32-bit version of launcher.</string>
|
||||
<string name="launcher_settings_icon_alt">settings icon</string>
|
||||
@@ -120,7 +120,7 @@
|
||||
<string name="title_activity_settings">Settings</string>
|
||||
<string name="back_icon_alt">back</string>
|
||||
|
||||
<string name="export_folder_tag">Launcher External Folder</string>
|
||||
<string name="export_folder_tag">Launcher external folder</string>
|
||||
<string name="text_copied">Copied to clipboard.</string>
|
||||
|
||||
<string name="preference_category_testing">General</string>
|
||||
@@ -129,6 +129,7 @@
|
||||
<string name="preferences_copy_external_button">Copy external files directory</string>
|
||||
<string name="preferences_open_file_manager">Open file manager</string>
|
||||
<string name="preferences_view_logs">View application logs</string>
|
||||
<string name="preferences_view_crashes">View crash dumps</string>
|
||||
|
||||
<string name="preference_theme_name">Theme</string>
|
||||
<string name="preference_theme_select">Select theme</string>
|
||||
@@ -136,6 +137,8 @@
|
||||
<string name="preference_theme_light">Light</string>
|
||||
<string name="preference_theme_dark">Dark</string>
|
||||
<string name="preference_black_background_name">Black background in dark theme</string>
|
||||
<string name="preference_disable_styles">Disable custom styling</string>
|
||||
<string name="preference_set_application_icon">Application icon</string>
|
||||
<string name="preference_force_hrr">Force maximum refresh rate</string>
|
||||
<string name="preference_enable_redesign">✨ Enable UI refresh</string>
|
||||
|
||||
@@ -219,6 +222,7 @@
|
||||
<string name="preference_text_clear">Clear</string>
|
||||
|
||||
<string name="preference_open_developer_options">Open developer options</string>
|
||||
<string name="preference_developer_options_dialog_title">Enable developer options?</string>
|
||||
|
||||
<string name="preference_profiles">Profiles</string>
|
||||
<string name="preference_profiles_create">Create profile</string>
|
||||
@@ -234,26 +238,48 @@
|
||||
<string name="preference_profiles_delete_undo_alt">Undo</string>
|
||||
|
||||
<!-- ApplicationLogsActivity strings -->
|
||||
<string name="title_activity_application_logs">Application Logs</string>
|
||||
<string name="title_activity_application_logs">Application logs</string>
|
||||
<string name="application_logs_no_logs">No logs available.</string>
|
||||
<string name="application_logs_delete">Clear All</string>
|
||||
<string name="application_logs_delete">Clear all</string>
|
||||
<string name="application_logs_share">Share</string>
|
||||
<string name="application_log_copy_label">Log Message</string>
|
||||
<string name="application_log_copy_label">Log message</string>
|
||||
<string name="application_logs_more">More</string>
|
||||
<string name="application_logs_export">Save To</string>
|
||||
<string name="application_logs_level">Set Log Level</string>
|
||||
<string name="application_logs_export">Save to</string>
|
||||
<string name="application_logs_level">Set log level</string>
|
||||
<string name="application_logs_level_dialog_name">Set minimum log level</string>
|
||||
<string name="application_logs_export_completed">Log export complete.</string>
|
||||
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
|
||||
<string name="application_logs_crash_only">Show Crash Buffer</string>
|
||||
<string name="application_logs_crash_only">Show crash buffer</string>
|
||||
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
|
||||
|
||||
<string name="title_activity_developer_settings">Developer Settings</string>
|
||||
<!-- DeveloperSettingsActivity strings -->
|
||||
<string name="title_activity_developer_settings">Developer options</string>
|
||||
<string name="preference_developer_mode">Enable developer options</string>
|
||||
<string name="preference_developer_mode_about">Developer mode enables some options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
|
||||
<string name="preference_developer_mode_about">Developer mode allows changing options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
|
||||
<string name="preference_developer_update_notice">Enabling developer mode automatically disallows the updater from overwriting custom loader builds.</string>
|
||||
|
||||
<string name="preference_override_exceptions_name">Custom exceptions fix symbols</string>
|
||||
<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>
|
||||
<string name="application_crashes_delete_one">Delete</string>
|
||||
|
||||
<!-- ApplicationIconActivity strings -->
|
||||
<string name="title_activity_application_icon">Application icons</string>
|
||||
<string name="application_icon_set_failed">Failed to change icon.</string>
|
||||
<string name="application_icon_default">Default</string>
|
||||
<string name="application_icon_geode">Geode</string>
|
||||
<string name="application_icon_pride">Pride</string>
|
||||
<string name="application_icon_trans">Transgender</string>
|
||||
|
||||
|
||||
</resources>
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
buildscript {
|
||||
val composeBOM by extra("2025.04.01")
|
||||
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.9.2" apply false
|
||||
id("com.android.library") version "8.9.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.20" apply false
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.1.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") {
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user