mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-15 13:16:43 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 469c690f5f | |||
| 0aacd0fadb | |||
| 19494bc7ac | |||
| 28a78d76fe | |||
| 6b3f8e1b62 | |||
| aacc40254c | |||
| efd26c3e4d | |||
| a8093c2a3c | |||
| 0f8b35c4cd | |||
| 866881d0e3 | |||
| a56b8d3de8 | |||
| e544cb3cf6 | |||
| ca9f2d43be | |||
| e10f55d9db | |||
| 71e035f83b | |||
| 16f2564478 | |||
| 75fda70db2 | |||
| b9e052b3a7 | |||
| 69aff83ef4 | |||
| e07e269bd7 | |||
| 08232ce642 | |||
| d536a66010 | |||
| 8907523ee4 | |||
| 9b1f6801a5 | |||
| ff03ef657a | |||
| 5acddfde16 | |||
| 913a74dbd3 | |||
| cbb92e75d3 | |||
| bd7d0e0466 | |||
| cce77b8d11 | |||
| c2f4449b1c |
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -104,8 +104,7 @@ cmake .. -G Ninja \
|
|||||||
-DYUZU_USE_QT_MULTIMEDIA=$MULTIMEDIA \
|
-DYUZU_USE_QT_MULTIMEDIA=$MULTIMEDIA \
|
||||||
-DYUZU_USE_QT_WEB_ENGINE=$WEBENGINE \
|
-DYUZU_USE_QT_WEB_ENGINE=$WEBENGINE \
|
||||||
-DYUZU_USE_FASTER_LD=ON \
|
-DYUZU_USE_FASTER_LD=ON \
|
||||||
-DYUZU_ENABLE_LTO=ON \
|
-DENABLE_LTO=ON \
|
||||||
-DDYNARMIC_ENABLE_LTO=ON \
|
|
||||||
"${EXTRA_CMAKE_FLAGS[@]}"
|
"${EXTRA_CMAKE_FLAGS[@]}"
|
||||||
|
|
||||||
ninja -j${NPROC}
|
ninja -j${NPROC}
|
||||||
|
|||||||
Regular → Executable
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash -ex
|
#!/bin/bash -ex
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
if [ "$COMPILER" == "clang" ]
|
if [ "$COMPILER" == "clang" ]
|
||||||
@@ -32,9 +32,8 @@ cmake .. -G Ninja \
|
|||||||
-DYUZU_ROOM_STANDALONE=OFF \
|
-DYUZU_ROOM_STANDALONE=OFF \
|
||||||
-DYUZU_USE_QT_MULTIMEDIA=${USE_MULTIMEDIA:-false} \
|
-DYUZU_USE_QT_MULTIMEDIA=${USE_MULTIMEDIA:-false} \
|
||||||
-DYUZU_USE_QT_WEB_ENGINE=${USE_WEBENGINE:-false} \
|
-DYUZU_USE_QT_WEB_ENGINE=${USE_WEBENGINE:-false} \
|
||||||
-DYUZU_ENABLE_LTO=ON \
|
-DENABLE_LTO=ON \
|
||||||
-DCMAKE_EXE_LINKER_FLAGS=" /LTCG" \
|
-DCMAKE_EXE_LINKER_FLAGS=" /LTCG" \
|
||||||
-DDYNARMIC_ENABLE_LTO=ON \
|
|
||||||
-DYUZU_USE_BUNDLED_QT=${BUNDLE_QT:-false} \
|
-DYUZU_USE_BUNDLED_QT=${BUNDLE_QT:-false} \
|
||||||
-DUSE_CCACHE=${CCACHE:-false} \
|
-DUSE_CCACHE=${CCACHE:-false} \
|
||||||
-DENABLE_UPDATE_CHECKER=${DEVEL:-true} \
|
-DENABLE_UPDATE_CHECKER=${DEVEL:-true} \
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
From b3622608433c183ba868a1dc8dd9cf285eb3b916 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dario Petrillo <dario.pk1@gmail.com>
|
||||||
|
Date: Thu, 27 Nov 2025 23:12:38 +0100
|
||||||
|
Subject: [PATCH] avoid extra memset when clearing an empty table
|
||||||
|
|
||||||
|
---
|
||||||
|
include/ankerl/unordered_dense.h | 6 ++++--
|
||||||
|
1 file changed, 4 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/include/ankerl/unordered_dense.h b/include/ankerl/unordered_dense.h
|
||||||
|
index 0835342..4938212 100644
|
||||||
|
--- a/include/ankerl/unordered_dense.h
|
||||||
|
+++ b/include/ankerl/unordered_dense.h
|
||||||
|
@@ -1490,8 +1490,10 @@ class table : public std::conditional_t<is_map_v<T>, base_table_type_map<T>, bas
|
||||||
|
// modifiers //////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
- m_values.clear();
|
||||||
|
- clear_buckets();
|
||||||
|
+ if (!empty()) {
|
||||||
|
+ m_values.clear();
|
||||||
|
+ clear_buckets();
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
auto insert(value_type const& value) -> std::pair<iterator, bool> {
|
||||||
+6
-4
@@ -32,8 +32,8 @@ if (PLATFORM_OPENBSD)
|
|||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R6/include -D_LIBCPP_PSTL_BACKEND_SERIAL=1")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R6/include -D_LIBCPP_PSTL_BACKEND_SERIAL=1")
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R6/lib")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R6/lib")
|
||||||
elseif (PLATFORM_NETBSD)
|
elseif (PLATFORM_NETBSD)
|
||||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R7/lib")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R7/lib")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -246,11 +246,12 @@ cmake_dependent_option(YUZU_USE_BUNDLED_MOLTENVK "Download bundled MoltenVK lib"
|
|||||||
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
|
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
|
||||||
|
|
||||||
set(DEFAULT_ENABLE_OPENSSL ON)
|
set(DEFAULT_ENABLE_OPENSSL ON)
|
||||||
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN)
|
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN OR PLATFORM_OPENBSD)
|
||||||
# - Windows defaults to the Schannel backend.
|
# - Windows defaults to the Schannel backend.
|
||||||
# - macOS defaults to the SecureTransport backend.
|
# - macOS defaults to the SecureTransport backend.
|
||||||
# - Android currently has no SSL backend as the NDK doesn't include any SSL
|
# - Android currently has no SSL backend as the NDK doesn't include any SSL
|
||||||
# library; a proper 'native' backend would have to go through Java.
|
# library; a proper 'native' backend would have to go through Java.
|
||||||
|
# - Solaris and OpenBSD have too old backends
|
||||||
# But you can force builds for those platforms to use OpenSSL if you have
|
# But you can force builds for those platforms to use OpenSSL if you have
|
||||||
# your own copy of it.
|
# your own copy of it.
|
||||||
set(DEFAULT_ENABLE_OPENSSL OFF)
|
set(DEFAULT_ENABLE_OPENSSL OFF)
|
||||||
@@ -263,7 +264,7 @@ endif()
|
|||||||
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
||||||
|
|
||||||
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
|
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
|
||||||
if (EXT_DEFAULT OR PLATFORM_SUN)
|
if (EXT_DEFAULT OR PLATFORM_SUN OR PLATFORM_OPENBSD)
|
||||||
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
|
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -575,6 +576,7 @@ add_subdirectory(externals)
|
|||||||
# pass targets from externals
|
# pass targets from externals
|
||||||
find_package(enet)
|
find_package(enet)
|
||||||
find_package(MbedTLS)
|
find_package(MbedTLS)
|
||||||
|
find_package(unordered_dense REQUIRED)
|
||||||
|
|
||||||
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
|
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
|
||||||
find_package(xbyak)
|
find_package(xbyak)
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ If you have `quazip1_qt6_devel`, uninstall it. It may call `Core5Compat` on CMak
|
|||||||
|
|
||||||
## OpenBSD
|
## OpenBSD
|
||||||
|
|
||||||
|
System boost doesn't have `context` (as of 7.8); so you may need to specify `-DYUZU_USE_CPM=ON -DBoost_FORCE_BUNDLED=ON`.
|
||||||
|
|
||||||
After configuration, you may need to modify `externals/ffmpeg/CMakeFiles/ffmpeg-build/build.make` to use `-j$(nproc)` instead of just `-j`.
|
After configuration, you may need to modify `externals/ffmpeg/CMakeFiles/ffmpeg-build/build.make` to use `-j$(nproc)` instead of just `-j`.
|
||||||
|
|
||||||
`-lc++-experimental` doesn't exist in OpenBSD but the LLVM driver still tries to link against it, to solve just symlink `ln -s /usr/lib/libc++.a /usr/lib/libc++experimental.a`. Builds are currently not working due to lack of `std::jthread` and such, either compile libc++ manually or wait for ports to catch up.
|
`-lc++-experimental` doesn't exist in OpenBSD but the LLVM driver still tries to link against it, to solve just symlink `ln -s /usr/lib/libc++.a /usr/lib/libc++experimental.a`. Builds are currently not working due to lack of `std::jthread` and such, either compile libc++ manually or wait for ports to catch up.
|
||||||
|
|||||||
+1
-1
@@ -277,7 +277,7 @@ For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
pkg_add -u
|
pkg_add -u
|
||||||
pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gmake llvm-19.1.7p3 qt6 jq fmt nlohmann-json enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl2 libusb1-1.0.27
|
pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gmake qt6 jq fmt nlohmann-json enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl2 libusb1-1.0.29
|
||||||
```
|
```
|
||||||
|
|
||||||
[Caveats](./Caveats.md#openbsd).
|
[Caveats](./Caveats.md#openbsd).
|
||||||
|
|||||||
+5
-5
@@ -82,15 +82,15 @@ You may additionally need the `Qt Extension Pack` extension if building Qt.
|
|||||||
|
|
||||||
# Build speedup
|
# Build speedup
|
||||||
|
|
||||||
If you have an HDD, use ramdisk (build in RAM):
|
If you have an HDD, use ramdisk (build in RAM), approximatedly you need 4GB for a full build with debug symbols:
|
||||||
```sh
|
```sh
|
||||||
sudo mkdir /tmp/ramdisk
|
mkdir /tmp/ramdisk
|
||||||
sudo chmod 777 /tmp/ramdisk
|
chmod 777 /tmp/ramdisk
|
||||||
# about 8GB needed
|
# about 8GB needed
|
||||||
sudo mount -t tmpfs -o size=8G myramdisk /tmp/ramdisk
|
mount -t tmpfs -o size=4G myramdisk /tmp/ramdisk
|
||||||
cmake -B /tmp/ramdisk
|
cmake -B /tmp/ramdisk
|
||||||
cmake --build /tmp/ramdisk -- -j32
|
cmake --build /tmp/ramdisk -- -j32
|
||||||
sudo umount /tmp/ramdisk
|
umount /tmp/ramdisk
|
||||||
```
|
```
|
||||||
|
|
||||||
# Assets and large files
|
# Assets and large files
|
||||||
|
|||||||
Vendored
+3
-3
@@ -66,6 +66,9 @@ if (NOT TARGET LLVM::Demangle)
|
|||||||
add_library(LLVM::Demangle ALIAS demangle)
|
add_library(LLVM::Demangle ALIAS demangle)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# unordered_dense
|
||||||
|
AddJsonPackage(unordered-dense)
|
||||||
|
|
||||||
if (YUZU_STATIC_ROOM)
|
if (YUZU_STATIC_ROOM)
|
||||||
return()
|
return()
|
||||||
endif()
|
endif()
|
||||||
@@ -244,9 +247,6 @@ if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER)
|
|||||||
AddJsonPackage(cpp-jwt)
|
AddJsonPackage(cpp-jwt)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# unordered_dense
|
|
||||||
AddJsonPackage(unordered-dense)
|
|
||||||
|
|
||||||
# FFMpeg
|
# FFMpeg
|
||||||
if (YUZU_USE_EXTERNAL_FFMPEG OR YUZU_USE_BUNDLED_FFMPEG)
|
if (YUZU_USE_EXTERNAL_FFMPEG OR YUZU_USE_BUNDLED_FFMPEG)
|
||||||
add_subdirectory(ffmpeg)
|
add_subdirectory(ffmpeg)
|
||||||
|
|||||||
Vendored
+6
-3
@@ -91,10 +91,13 @@
|
|||||||
"unordered-dense": {
|
"unordered-dense": {
|
||||||
"package": "unordered_dense",
|
"package": "unordered_dense",
|
||||||
"repo": "martinus/unordered_dense",
|
"repo": "martinus/unordered_dense",
|
||||||
"tag": "v%VERSION%",
|
"sha": "7b55cab841",
|
||||||
"hash": "b98b5d4d96f8e0081b184d6c4c1181fae4e41723b54bed4296717d7f417348b48fad0bbcc664cac142b8c8a47e95aa57c1eb1cf6caa855fd782fad3e3ab99e5e",
|
"hash": "d2106f6640f6bfb81755e4b8bfb64982e46ec4a507cacdb38f940123212ccf35a20b43c70c6f01d7bfb8c246d1a16f7845d8052971949cea9def1475e3fa02c8",
|
||||||
"find_args": "CONFIG",
|
"find_args": "CONFIG",
|
||||||
"git_version": "4.8.1"
|
"bundled": true,
|
||||||
|
"patches": [
|
||||||
|
"0001-avoid-memset-when-clearing-an-empty-table.patch"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"mbedtls": {
|
"mbedtls": {
|
||||||
"package": "MbedTLS",
|
"package": "MbedTLS",
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -33,7 +33,7 @@ endif()
|
|||||||
|
|
||||||
if(NOT YUZU_TZDB_PATH STREQUAL "")
|
if(NOT YUZU_TZDB_PATH STREQUAL "")
|
||||||
set(NX_TZDB_BASE_DIR "${YUZU_TZDB_PATH}")
|
set(NX_TZDB_BASE_DIR "${YUZU_TZDB_PATH}")
|
||||||
elseif (MSVC AND NOT CXX_CLANG AND YUZU_ENABLE_LTO)
|
elseif (MSVC AND NOT CXX_CLANG AND ENABLE_LTO)
|
||||||
# TODO(crueter): boot up the windows vm
|
# TODO(crueter): boot up the windows vm
|
||||||
set(NX_TZDB_VERSION "250725")
|
set(NX_TZDB_VERSION "250725")
|
||||||
set(NX_TZDB_ARCHIVE "${CPM_SOURCE_CACHE}/nx_tzdb/${NX_TZDB_VERSION}.zip")
|
set(NX_TZDB_ARCHIVE "${CPM_SOURCE_CACHE}/nx_tzdb/${NX_TZDB_VERSION}.zip")
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
// import android.annotation.SuppressLint
|
// import android.annotation.SuppressLint
|
||||||
import com.android.build.gradle.api.ApplicationVariant
|
|
||||||
import kotlin.collections.setOf
|
|
||||||
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
|
||||||
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
|
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
|
||||||
import org.gradle.api.tasks.Copy
|
import org.gradle.api.tasks.Copy
|
||||||
@@ -15,7 +13,8 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.9.20"
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
|
kotlin("plugin.serialization") version "2.3.0"
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
|
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
|
||||||
id("com.github.triplet.play") version "3.8.6"
|
id("com.github.triplet.play") version "3.8.6"
|
||||||
@@ -43,6 +42,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
compose = true
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -50,8 +50,10 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlin {
|
||||||
jvmTarget = "17"
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
@@ -65,7 +67,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "dev.eden.eden_emulator"
|
applicationId = "dev.eden.eden_emulator"
|
||||||
minSdk = 24
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionName = getGitVersion()
|
versionName = getGitVersion()
|
||||||
versionCode = autoVersion
|
versionCode = autoVersion
|
||||||
@@ -267,6 +269,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composeCompiler {
|
||||||
|
reportsDestination = layout.buildDirectory.dir("compose_compiler")
|
||||||
|
}
|
||||||
|
|
||||||
idea {
|
idea {
|
||||||
module {
|
module {
|
||||||
// Inclusion to exclude build/ dir from non-Android
|
// Inclusion to exclude build/ dir from non-Android
|
||||||
@@ -292,7 +298,7 @@ tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
|
|||||||
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
|
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
|
||||||
|
|
||||||
ktlint {
|
ktlint {
|
||||||
version.set("0.47.1")
|
version.set("0.50.0")
|
||||||
android.set(true)
|
android.set(true)
|
||||||
ignoreFailures.set(false)
|
ignoreFailures.set(false)
|
||||||
disabledRules.set(
|
disabledRules.set(
|
||||||
@@ -317,29 +323,42 @@ play {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.15.0")
|
implementation("androidx.compose:compose-bom:2026.01.01")
|
||||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
implementation("androidx.compose.ui:ui")
|
||||||
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
|
implementation("androidx.compose.material3:material3:1.4.0")
|
||||||
|
implementation("androidx.activity:activity-compose:1.12.3")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
|
||||||
|
implementation("androidx.navigation:navigation-compose:2.9.7")
|
||||||
|
implementation("io.coil-kt:coil-compose:2.7.0")
|
||||||
|
implementation("io.coil-kt:coil-svg:2.7.0")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
|
|
||||||
|
implementation("androidx.core:core-ktx:1.17.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.8.6")
|
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
||||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
implementation("androidx.documentfile:documentfile:1.1.0")
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.13.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:5.3.2")
|
||||||
implementation("io.coil-kt:coil:2.2.2")
|
implementation("io.coil-kt:coil:2.7.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.2.0")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.21.0")
|
||||||
implementation("androidx.window:window:1.3.0")
|
implementation("androidx.window:window:1.5.1")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0")
|
||||||
implementation("org.commonmark:commonmark:0.22.0")
|
implementation("org.commonmark:commonmark:0.27.1")
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.8.9")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.9.7")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.8.9")
|
implementation("androidx.navigation:navigation-ui-ktx:2.9.7")
|
||||||
implementation("info.debatty:java-string-similarity:2.0.0")
|
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
|
||||||
implementation("androidx.compose.ui:ui-graphics-android:1.7.8")
|
implementation("androidx.compose.ui:ui-graphics-android:1.10.2")
|
||||||
implementation("androidx.compose.ui:ui-text-android:1.7.8")
|
implementation("androidx.compose.ui:ui-text-android:1.10.2")
|
||||||
implementation("net.swiftzer.semver:semver:2.0.0")
|
implementation("net.swiftzer.semver:semver:2.1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runGitCommand(command: List<String>): String {
|
fun runGitCommand(command: List<String>): String {
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg7"
|
||||||
|
sodipodi:docname="base.svg.2026_01_12_14_43_47.0.svg"
|
||||||
|
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||||
|
inkscape:export-filename="base.svg.2026_01_12_14_43_47.0.svg"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs7">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient1"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ff2e88;stop-opacity:0.5;"
|
||||||
|
offset="0"
|
||||||
|
id="stop3" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#bf42f6;stop-opacity:0.5;"
|
||||||
|
offset="0.44631511"
|
||||||
|
id="stop4" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#5da5ed;stop-opacity:0.5;"
|
||||||
|
offset="0.90088946"
|
||||||
|
id="stop2" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient138"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ff2e88;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop152" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#bf42f6;stop-opacity:1;"
|
||||||
|
offset="0.44971901"
|
||||||
|
id="stop137" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#5da5ed;stop-opacity:1;"
|
||||||
|
offset="0.89793283"
|
||||||
|
id="stop138" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="swatch37"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop37" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="swatch28"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#252525;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop28" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="swatch27"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000000;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop27" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="swatch15"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop16" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient14"
|
||||||
|
inkscape:swatch="gradient">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop14" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop15" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="swatch9"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop10" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="swatch8"
|
||||||
|
inkscape:swatch="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop9" />
|
||||||
|
</linearGradient>
|
||||||
|
<rect
|
||||||
|
x="22.627417"
|
||||||
|
y="402.76802"
|
||||||
|
width="521.34025"
|
||||||
|
height="248.94868"
|
||||||
|
id="rect24" />
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient11"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ff2e88;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop11" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#bf42f6;stop-opacity:1;"
|
||||||
|
offset="0.44971901"
|
||||||
|
id="stop154" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#5da5ed;stop-opacity:1;"
|
||||||
|
offset="0.89793283"
|
||||||
|
id="stop12" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient138"
|
||||||
|
id="linearGradient6"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(1.118028,0,0,1.116699,-46.314723,-42.388667)"
|
||||||
|
x1="270.39996"
|
||||||
|
y1="40.000019"
|
||||||
|
x2="270.39996"
|
||||||
|
y2="494.39996"
|
||||||
|
spreadMethod="pad" />
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath18">
|
||||||
|
<circle
|
||||||
|
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
|
||||||
|
id="circle18"
|
||||||
|
cx="-246.8315"
|
||||||
|
cy="246.8338"
|
||||||
|
inkscape:label="Circle"
|
||||||
|
r="191.89999" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath22">
|
||||||
|
<circle
|
||||||
|
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
|
||||||
|
id="circle22"
|
||||||
|
cx="256"
|
||||||
|
cy="256"
|
||||||
|
inkscape:label="Circle"
|
||||||
|
r="191.89999" />
|
||||||
|
</clipPath>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient11"
|
||||||
|
id="linearGradient27"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(-6.9401139e-5,-2.8678628)"
|
||||||
|
x1="256.00012"
|
||||||
|
y1="102.94693"
|
||||||
|
x2="256.00012"
|
||||||
|
y2="409.05307" />
|
||||||
|
<clipPath
|
||||||
|
clipPathUnits="userSpaceOnUse"
|
||||||
|
id="clipPath128">
|
||||||
|
<circle
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#03ffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="circle128"
|
||||||
|
cx="256"
|
||||||
|
cy="256"
|
||||||
|
r="192" />
|
||||||
|
</clipPath>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient1"
|
||||||
|
id="linearGradient2"
|
||||||
|
x1="256"
|
||||||
|
y1="64"
|
||||||
|
x2="256"
|
||||||
|
y2="448"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(1.3229974,0,0,1.3214002,-82.687336,-82.290326)" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:cx="261.62951"
|
||||||
|
inkscape:cy="230.87036"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1008"
|
||||||
|
inkscape:window-x="1080"
|
||||||
|
inkscape:window-y="351"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg7" />
|
||||||
|
<path
|
||||||
|
id="path8-7"
|
||||||
|
style="display:inline;mix-blend-mode:multiply;fill:url(#linearGradient6);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2);stroke-width:3.9666;stroke-dasharray:none;stroke-opacity:0.566238;paint-order:stroke fill markers"
|
||||||
|
inkscape:label="Circle"
|
||||||
|
d="M 256,2.2792898 A 254.0155,253.71401 0 0 0 150.68475,25.115202 c 19.54414,1.070775 38.74692,5.250294 51.56848,11.647658 14.14361,7.056691 28.63804,19.185961 39.4212,29.347551 h 40.60981 c 1.03847,-0.68139 2.10297,-1.36938 3.1938,-2.05957 5.45602,-15.78533 14.79164,-43.183497 19.49612,-57.0097682 A 254.0155,253.71401 0 0 0 256,2.2792898 Z m 61.57106,7.567234 -18.26098,46.1544672 c 7.79702,-4.13918 16.35655,-7.87447 25.20671,-10.87081 23.1229,-7.828433 43.96931,-10.170904 54.94058,-10.868226 A 254.0155,253.71401 0 0 0 317.57106,9.8465238 Z m 65.39277,26.4001532 c -9.68256,4.806644 -33.05532,16.642034 -55.68217,29.863734 H 424.4677 A 254.0155,253.71401 0 0 0 382.96383,36.246677 Z M 113.90698,45.690231 A 254.0155,253.71401 0 0 0 87.532302,66.110411 H 194.2739 c -1.47402,-0.80231 -2.35141,-1.25949 -2.35141,-1.25949 l 10.4496,-11.83348 -38.40568,7.01234 c 0,1e-5 -12.21537,-4.60266 -40.17313,-12.27223 -3.45336,-0.94731 -6.75329,-1.61824 -9.8863,-2.06732 z m -36.803618,30.18635 a 254.0155,253.71401 0 0 0 -34.88372,43.090929 h 59.976738 c 18.11461,-12.04145 40.14252,-22.882149 62.31266,-24.534159 52.93006,-3.9444 70.16538,1.86342 70.16538,1.86342 0,0 -4.612,-4.8206 -14.51938,-13.36656 -2.72366,-2.34942 -6.0844,-4.77373 -9.52455,-7.05363 z m 174.472868,0 c 4.57322,4.7186 7.29716,7.83565 7.29716,7.83565 0,0 3.53501,-3.18484 9.62532,-7.83565 z m 60.27649,0 c -21.56573,15.45339 -25.4703,27.979669 -25.4703,27.979669 0,0 54.83326,-19.215729 100.70543,-0.31228 11.63986,4.79661 21.58481,10.13159 29.94832,15.42354 h 52.74419 A 254.0155,253.71401 0 0 0 434.89664,75.876581 Z M 36.250648,128.73367 A 254.0155,253.71401 0 0 0 16.372095,171.82459 H 147.45478 c 1.45695,-2.5815 3.06539,-5.08648 4.83979,-7.48982 14.23694,-19.28301 27.92088,-30.0088 36.86047,-35.6011 h -30.25323 c -5.87346,0.93472 -12.04945,1.99094 -18.28166,3.16937 -30.12936,5.69727 -81.157618,22.78945 -81.157618,22.78945 0,0 11.47125,-12.39249 29.11369,-25.95882 z m 265.630492,0 c 33.48676,11.2434 52.42799,26.78443 62.7752,43.09092 h 130.97157 a 254.0155,253.71401 0 0 0 -19.87856,-43.09092 h -44.81136 c 14.85233,11.5863 21.59948,20.9854 21.59948,20.9854 0,0 -33.5226,-12.37087 -66.0646,-20.9854 z m -45.96641,16.27007 c -1.00419,0.0106 -10.12705,0.72026 -44.98966,20.64729 -3.12132,1.78406 -6.25434,3.86182 -9.37468,6.17356 h 41.81911 c 7.17181,-17.34774 12.64083,-26.82085 12.64083,-26.82085 0,0 -0.0287,-7.1e-4 -0.0957,0 z m 14.18088,0.0465 c 0,0 -3.31228,9.32762 -7.30492,26.77438 h 51.78554 C 287.6577,146.14158 270.09561,145.0502 270.09561,145.0502 Z M 13.152456,181.59075 A 254.0155,253.71401 0 0 0 3.927651,224.68167 H 134.1447 c 0.56161,-12.72411 2.67825,-28.50188 8.61499,-43.09092 z m 176.661504,0 c -14.27121,13.10564 -27.60733,29.58761 -37.56073,43.09092 h 73.3721 c 4.47018,-16.79061 9.35068,-31.26371 13.86562,-43.09092 z m 70.85787,0 c -2.41384,11.76417 -4.9032,26.20707 -6.94831,43.09092 H 360.4832 c -8.32133,-10.88917 -20.66988,-26.17008 -36.35141,-43.09092 z m 109.17313,0 c 6.63611,15.24089 6.92441,30.5373 5.57882,43.09092 h 132.64857 a 254.0155,253.71401 0 0 0 -9.22481,-43.09092 z M 2.90181,234.44783 A 254.0155,253.71401 0 0 0 1.984498,255.9933 254.0155,253.71401 0 0 0 2.90181,277.53876 h 211.89923 c 2.25762,-15.52555 5.14325,-29.93448 8.3385,-43.09093 h -77.8863 c -6.46396,9.27617 -10.33076,15.56549 -10.33076,15.56549 0,0 -0.82623,-6.14945 -0.9354,-15.56549 z m 249.72093,0 c -1.3692,13.09684 -2.4456,27.49209 -3.02068,43.09093 h 259.49613 a 254.0155,253.71401 0 0 0 0.91731,-21.54546 254.0155,253.71401 0 0 0 -0.91731,-21.54547 H 374.02584 c -0.445,2.5469 -0.90878,4.89768 -1.32817,7.01751 0,0 -1.69726,-2.53821 -4.94056,-7.01751 z M 3.927651,287.30493 a 254.0155,253.71401 0 0 0 9.224805,43.09091 H 214.04393 c -1.29238,-15.40742 -1.57503,-30.04388 -0.41861,-43.09091 z m 245.385009,0 c -0.30355,13.54349 -0.22032,27.92598 0.36951,43.09091 h 249.16537 a 254.0155,253.71401 0 0 0 9.22481,-43.09091 z M 16.369511,340.16201 a 254.0155,253.71401 0 0 0 19.878554,43.09091 H 221.4677 c -2.69781,-14.4523 -4.96108,-29.01285 -6.4832,-43.09091 z m 233.842379,0 c 1.15864,15.47765 3.81286,29.83979 7.51679,43.09091 h 218.02325 a 254.0155,253.71401 0 0 0 19.87856,-43.09091 z M 42.217052,393.01909 a 254.0155,253.71401 0 0 0 34.88372,43.09093 H 233.09561 c -3.40902,-13.67281 -6.76794,-28.2531 -9.73902,-43.09093 z m 218.490958,0 c 5.34985,16.15926 12.22007,30.51982 19.68733,43.09093 h 154.50389 a 254.0155,253.71401 0 0 0 34.88371,-43.09093 z M 87.529722,445.87618 a 254.0155,253.71401 0 0 0 166.229968,63.8208 c -3.67805,-12.0825 -10.85464,-35.49828 -18.18088,-63.8208 z m 199.010328,0 c 17.5887,26.43772 36.99259,43.60598 47.33592,51.61309 a 254.0155,253.71401 0 0 0 90.59431,-51.61309 z" />
|
||||||
|
<path
|
||||||
|
id="path27"
|
||||||
|
style="display:inline;mix-blend-mode:multiply;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient27);stroke-width:3;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
|
||||||
|
d="m 318.98012,441.7375 c -9.87518,-6.73978 -64.39137,-49.0272 -67.68975,-127.81978 -3.69298,-88.21893 15.36468,-141.91029 15.36468,-141.91029 0,0 16.00378,0.99513 39.80316,26.53195 23.79939,25.53753 37.74965,46.43102 37.74965,46.43102 3.91262,-19.79992 12.84563,-66.32402 -60.72865,-87.55523 0,0 12.82326,-5.38883 39.3925,-3.81382 26.56907,1.57572 81.6822,21.93799 81.6822,21.93799 0,0 -14.79766,-20.63773 -49.47063,-34.94295 -34.67291,-14.30533 -76.1182,0.23644 -76.1182,0.23644 0,0 3.86959,-12.43127 27.22669,-26.38478 23.35718,-13.9537 49.27409,-26.501533 49.27409,-26.501533 0,0 -21.97854,-0.26548 -47.67725,8.44535 -6.68948,2.267506 -13.15863,5.094213 -19.05208,8.226563 l 16.05803,-40.634103 -4.4617,-1.89059 -5.1305,-0.95965 c 0,0 -11.24072,33.12428 -16.92051,49.576513 -12.13137,7.68489 -20.11005,14.87735 -20.11005,14.87735 0,0 -21.90573,-25.09227 -42.79668,-35.527803 -26.03412,-13.00525 -86.88249,-13.90359 -94.0044,10.401173 0,0 13.56804,-7.884703 34.70032,-2.080917 21.13214,5.803997 30.3644,9.287307 30.3644,9.287307 l 29.02989,-5.30681 -7.89811,8.95527 c 0,0 13.8496,7.21324 21.33822,13.68063 7.48859,6.46722 10.9757,10.11472 10.9757,10.11472 0,0 -13.02739,-4.39388 -53.03507,-1.40893 -40.00771,2.98473 -79.40016,45.60209 -79.40016,45.60209 0,0 38.57037,-12.93531 61.34393,-17.24677 22.77354,-4.31126 44.52166,-6.46757 44.52166,-6.46757 0,0 -17.23298,5.97003 -35.69792,31.00932 -18.46522,25.03987 -13.13146,64.83866 -13.13146,64.83866 0,0 29.33874,-47.7577 57.44675,-63.84249 28.10798,-16.08527 34.0799,-15.6238 34.0799,-15.6238 0,0 -22.56785,39.13486 -31.39017,101.98268 -8.03005,57.2039 26.77689,163.75449 31.1572,178.89699"
|
||||||
|
sodipodi:nodetypes="cscsccscscscsccccccscscccscscscscscsc"
|
||||||
|
inkscape:label="MainOutline"
|
||||||
|
clip-path="url(#clipPath128)"
|
||||||
|
transform="matrix(1.3229974,0,0,1.3214002,-82.687282,-82.278451)" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,184 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.background
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.decode.SvgDecoder
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RetroGridBackground(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
lineColor: Color = EdenTheme.colors.primary.copy(alpha = 0.2f),
|
||||||
|
spacing: Dp = 40.dp,
|
||||||
|
lineWidth: Dp = 2.dp,
|
||||||
|
speedDpPerSecond: Float = 12f,
|
||||||
|
fadeColor: Color = EdenTheme.colors.background,
|
||||||
|
logoScale: Float = 0.35f,
|
||||||
|
logoAlpha: Float = 0.25f,
|
||||||
|
horizonRatio: Float = 0.45f,
|
||||||
|
logoAssetPath: String = "file:///android_asset/base.svg", // correct way?
|
||||||
|
logoWidth: Dp? = null,
|
||||||
|
logoHeight: Dp? = null,
|
||||||
|
logoCropBottomRatio: Float = 0.33f,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val durationMs = remember(spacing, speedDpPerSecond, density) {
|
||||||
|
val spacingPx = spacing.value * density.density
|
||||||
|
val speedPxPerSecond = speedDpPerSecond * density.density
|
||||||
|
((spacingPx / speedPxPerSecond) * 1000f).toInt().coerceAtLeast(300)
|
||||||
|
}
|
||||||
|
|
||||||
|
val transition = rememberInfiniteTransition(label = "grid")
|
||||||
|
val phase by transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Restart,
|
||||||
|
),
|
||||||
|
label = "gridPhase",
|
||||||
|
)
|
||||||
|
|
||||||
|
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
|
||||||
|
val baseSize = maxWidth * logoScale
|
||||||
|
val resolvedWidth = logoWidth ?: baseSize
|
||||||
|
val resolvedHeight = logoHeight ?: baseSize
|
||||||
|
val cropRatio = logoCropBottomRatio.coerceIn(0f, 0.9f)
|
||||||
|
val visibleHeight = resolvedHeight * (1f - cropRatio)
|
||||||
|
val visibleHeightPx = with(density) { visibleHeight.toPx() }
|
||||||
|
val horizonY = maxHeight * horizonRatio
|
||||||
|
val logoOffsetY = horizonY - (resolvedHeight * 0.5f) - 50.dp
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.drawWithCache {
|
||||||
|
val spacingPx = spacing.toPx().coerceAtLeast(8f)
|
||||||
|
val strokePx = lineWidth.toPx().coerceAtLeast(1f)
|
||||||
|
val horizonScale = 0.18f
|
||||||
|
val overscan = 5.0f
|
||||||
|
val perspectivePower = 2.2f
|
||||||
|
onDrawBehind {
|
||||||
|
val width = size.width
|
||||||
|
val height = size.height
|
||||||
|
val centerX = width * 0.5f
|
||||||
|
val horizonYpx = height * horizonRatio
|
||||||
|
val depth = (height - horizonYpx).coerceAtLeast(1f)
|
||||||
|
val offset = (1f - phase) * spacingPx
|
||||||
|
val baseHalf = centerX * overscan
|
||||||
|
val horizonHalf = baseHalf * horizonScale
|
||||||
|
val lineCount = (depth / (spacingPx * 0.65f))
|
||||||
|
.toInt()
|
||||||
|
.coerceAtLeast(20)
|
||||||
|
|
||||||
|
var x = centerX - baseHalf - spacingPx * 6f
|
||||||
|
while (x <= centerX + baseHalf + spacingPx * 6f) {
|
||||||
|
val endX = centerX + (x - centerX) * horizonScale
|
||||||
|
drawLine(
|
||||||
|
color = lineColor,
|
||||||
|
start = Offset(x, height),
|
||||||
|
end = Offset(endX, horizonYpx),
|
||||||
|
strokeWidth = strokePx,
|
||||||
|
)
|
||||||
|
x += spacingPx
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (i <= lineCount) {
|
||||||
|
val z = ((i + (offset / spacingPx)) / lineCount.toFloat())
|
||||||
|
.coerceIn(0f, 1f)
|
||||||
|
val zCurve = z.toDouble().pow(perspectivePower.toDouble()).toFloat()
|
||||||
|
val y = horizonYpx + depth * zCurve
|
||||||
|
val halfWidth = horizonHalf + (baseHalf - horizonHalf) * z
|
||||||
|
drawLine(
|
||||||
|
color = lineColor,
|
||||||
|
start = Offset(centerX - halfWidth, y),
|
||||||
|
end = Offset(centerX + halfWidth, y),
|
||||||
|
strokeWidth = strokePx,
|
||||||
|
)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val fadeHeight = height * 0.12f
|
||||||
|
val solidHeight = (horizonYpx - fadeHeight).coerceAtLeast(0f)
|
||||||
|
if (solidHeight > 0f) {
|
||||||
|
drawRect(
|
||||||
|
color = fadeColor,
|
||||||
|
topLeft = Offset(0f, 0f),
|
||||||
|
size = Size(width, solidHeight),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(fadeColor, Color.Transparent),
|
||||||
|
startY = horizonYpx - fadeHeight,
|
||||||
|
endY = horizonYpx + fadeHeight,
|
||||||
|
),
|
||||||
|
topLeft = Offset(0f, horizonYpx - fadeHeight),
|
||||||
|
size = Size(width, fadeHeight * 2f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(context)
|
||||||
|
.data(logoAssetPath)
|
||||||
|
.decoderFactory(SvgDecoder.Factory())
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.requiredWidth(resolvedWidth)
|
||||||
|
.requiredHeight(resolvedHeight)
|
||||||
|
.offset(y = logoOffsetY)
|
||||||
|
.drawWithContent {
|
||||||
|
clipRect(
|
||||||
|
left = 0f,
|
||||||
|
top = 0f,
|
||||||
|
right = size.width,
|
||||||
|
bottom = visibleHeightPx,
|
||||||
|
) {
|
||||||
|
this@drawWithContent.drawContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphicsLayer(alpha = logoAlpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.MarqueeAnimationMode
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.role
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import dev.eden.emu.ui.theme.Shapes
|
||||||
|
import dev.eden.emu.ui.utils.ConfirmKeys
|
||||||
|
import dev.eden.emu.ui.utils.MenuKeys
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun EdenTile(
|
||||||
|
title: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
imageUri: String? = null,
|
||||||
|
iconSize: Dp = 140.dp,
|
||||||
|
tileHeight: Dp = 168.dp,
|
||||||
|
useLargerFont: Boolean = false,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||||
|
val borderColor = if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.width(iconSize)
|
||||||
|
.height(tileHeight)
|
||||||
|
.semantics { role = Role.Button }
|
||||||
|
.onPreviewKeyEvent { e ->
|
||||||
|
if (e.type == KeyEventType.KeyUp) {
|
||||||
|
when {
|
||||||
|
e.key in ConfirmKeys -> { onClick(); true }
|
||||||
|
e.key in MenuKeys -> { onLongClick?.invoke(); true }
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
.focusable(interactionSource = interactionSource)
|
||||||
|
.combinedClickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
// Game icon
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(iconSize)
|
||||||
|
.background(EdenTheme.colors.surface, Shapes.medium)
|
||||||
|
.border(BorderStroke(3.dp, borderColor), Shapes.medium)
|
||||||
|
.clip(Shapes.medium),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (imageUri.isNullOrBlank()) {
|
||||||
|
BasicText(
|
||||||
|
text = title.take(1).uppercase(Locale.getDefault()),
|
||||||
|
style = EdenTheme.typography.title.copy(color = EdenTheme.colors.onSurface),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
android.widget.ImageView(ctx).apply {
|
||||||
|
layoutParams = android.view.ViewGroup.LayoutParams(-1, -1)
|
||||||
|
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
||||||
|
setImageResource(R.drawable.default_icon)
|
||||||
|
GameIconUtils.loadGameIcon(Game(title, imageUri, "0", "", "", false), this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game title with marquee
|
||||||
|
MarqueeText(title, isFocused, 32.dp, useLargerFont, Modifier.width(iconSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MarqueeText(
|
||||||
|
text: String,
|
||||||
|
isAnimating: Boolean,
|
||||||
|
height: Dp,
|
||||||
|
useLargerFont: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val style = if (useLargerFont) {
|
||||||
|
EdenTheme.typography.body.copy(Color.White, 18.sp, fontWeight = FontWeight.Bold)
|
||||||
|
} else {
|
||||||
|
EdenTheme.typography.label.copy(Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier.height(height).clipToBounds(), Alignment.Center) {
|
||||||
|
BasicText(
|
||||||
|
text = text,
|
||||||
|
style = style,
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = if (isAnimating) {
|
||||||
|
Modifier.basicMarquee(
|
||||||
|
iterations = Int.MAX_VALUE,
|
||||||
|
animationMode = MarqueeAnimationMode.Immediately,
|
||||||
|
initialDelayMillis = 150, // does not seem to take effect
|
||||||
|
repeatDelayMillis = 1000,
|
||||||
|
velocity = 30.dp,
|
||||||
|
)
|
||||||
|
} else Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import dev.eden.emu.ui.theme.Dimens
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import dev.eden.emu.ui.utils.ConfirmKeys
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expandable search bar for controller-based navigation.
|
||||||
|
* ________________________________________________________________
|
||||||
|
* Note: Focus Manager on this doesn't work good. Need a hide impl.
|
||||||
|
* when it looses focus for better Controller Experience
|
||||||
|
* ________________________________________________________________
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ExpandableSearchBar(
|
||||||
|
query: String,
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val searchHint = context.getString(R.string.home_search_games)
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
val iconInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isIconFocused by iconInteractionSource.collectIsFocusedAsState()
|
||||||
|
|
||||||
|
// Track if we have an active filter (submitted search, if keyboard just hides, it kinda breaks)
|
||||||
|
val hasActiveFilter = query.isNotEmpty() && !isExpanded
|
||||||
|
val textFieldFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
// Focus text field when expanded
|
||||||
|
LaunchedEffect(isExpanded) {
|
||||||
|
if (isExpanded) {
|
||||||
|
delay(100)
|
||||||
|
try {
|
||||||
|
textFieldFocusRequester.requestFocus()
|
||||||
|
keyboardController?.show()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a fixed width container to prevent layout shift.
|
||||||
|
Box(
|
||||||
|
modifier = modifier.width(if (isExpanded) 300.dp else 48.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
) {
|
||||||
|
if (isExpanded) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(300.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(EdenTheme.colors.surface)
|
||||||
|
.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = EdenTheme.colors.primary,
|
||||||
|
shape = RoundedCornerShape(24.dp)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
// Text input - NO onFocusChanged to prevent auto-close issues
|
||||||
|
// (Needs to be "submitted", hiding keyboard breaks focus flow)
|
||||||
|
BasicTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = onQueryChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.focusRequester(textFieldFocusRequester)
|
||||||
|
.onPreviewKeyEvent { keyEvent ->
|
||||||
|
if (keyEvent.type == KeyEventType.KeyUp) {
|
||||||
|
when (keyEvent.key) {
|
||||||
|
Key.Escape, Key.Back -> {
|
||||||
|
onQueryChange("")
|
||||||
|
onExpandedChange(false)
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.Enter, Key.NumPadEnter -> {
|
||||||
|
keyboardController?.hide()
|
||||||
|
onExpandedChange(false)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} else false
|
||||||
|
},
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(EdenTheme.colors.primary),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onSearch = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
onExpandedChange(false)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(vertical = 14.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
androidx.compose.foundation.text.BasicText(
|
||||||
|
text = searchHint,
|
||||||
|
style = TextStyle(
|
||||||
|
color = Color.White.copy(alpha = 0.5f),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clear/Close button (X icon)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable {
|
||||||
|
onQueryChange("")
|
||||||
|
onExpandedChange(false)
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
.padding(8.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_clear),
|
||||||
|
contentDescription = context.getString(R.string.home_clear),
|
||||||
|
tint = Color.White.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Collapsed state: search icon
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
if (isIconFocused) EdenTheme.colors.primary
|
||||||
|
else EdenTheme.colors.surface
|
||||||
|
)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.focusable(interactionSource = iconInteractionSource)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = iconInteractionSource,
|
||||||
|
indication = null,
|
||||||
|
) {
|
||||||
|
if (hasActiveFilter) {
|
||||||
|
onQueryChange("")
|
||||||
|
} else {
|
||||||
|
onExpandedChange(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onPreviewKeyEvent { keyEvent ->
|
||||||
|
if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key in ConfirmKeys) {
|
||||||
|
if (hasActiveFilter) {
|
||||||
|
onQueryChange("")
|
||||||
|
} else {
|
||||||
|
onExpandedChange(true)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
|
||||||
|
contentDescription = context.getString(R.string.home_search),
|
||||||
|
tint = if (hasActiveFilter) EdenTheme.colors.primary else Color.White,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Active filter indicator dot
|
||||||
|
if (hasActiveFilter) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(6.dp)
|
||||||
|
.size(10.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(EdenTheme.colors.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.snapping.SnapPosition
|
||||||
|
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GameCarousel(
|
||||||
|
gameTiles: List<GameTile>,
|
||||||
|
onGameClick: (GameTile) -> Unit,
|
||||||
|
onGameLongClick: (GameTile) -> Unit = {},
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
onNavigateUp: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
initialFocusedIndex: Int = 0,
|
||||||
|
onFocusedIndexChanged: (Int) -> Unit = {},
|
||||||
|
onShowGameInfo: (GameTile) -> Unit = {},
|
||||||
|
) {
|
||||||
|
if (gameTiles.isEmpty()) return
|
||||||
|
|
||||||
|
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
|
||||||
|
|
||||||
|
val listState = rememberLazyListState(
|
||||||
|
initialFirstVisibleItemIndex = safeInitialIndex,
|
||||||
|
initialFirstVisibleItemScrollOffset = 0
|
||||||
|
)
|
||||||
|
val snapFlingBehavior = rememberSnapFlingBehavior(
|
||||||
|
lazyListState = listState,
|
||||||
|
snapPosition = SnapPosition.Center
|
||||||
|
)
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var centerItemIndex by remember { mutableIntStateOf(safeInitialIndex) }
|
||||||
|
|
||||||
|
LaunchedEffect(centerItemIndex) {
|
||||||
|
onFocusedIndexChanged(centerItemIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun centerOnIndex(index: Int, animate: Boolean = true) {
|
||||||
|
if (gameTiles.isEmpty()) return
|
||||||
|
if (listState.layoutInfo.visibleItemsInfo.none { it.index == index }) {
|
||||||
|
if (animate) {
|
||||||
|
listState.animateScrollToItem(index)
|
||||||
|
} else {
|
||||||
|
listState.scrollToItem(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(10)
|
||||||
|
val layoutInfo = listState.layoutInfo
|
||||||
|
val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return
|
||||||
|
val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2
|
||||||
|
val desiredStart = viewportCenter - itemInfo.size / 2
|
||||||
|
if (itemInfo.offset != desiredStart) {
|
||||||
|
val scrollOffset = -(viewportCenter - itemInfo.size / 2)
|
||||||
|
if (animate) {
|
||||||
|
listState.animateScrollToItem(index, scrollOffset = scrollOffset)
|
||||||
|
} else {
|
||||||
|
listState.scrollToItem(index, scrollOffset = scrollOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
listState.scrollToItem(safeInitialIndex)
|
||||||
|
delay(50)
|
||||||
|
centerOnIndex(safeInitialIndex, animate = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val focusRequesters = remember(gameTiles.size) {
|
||||||
|
List(gameTiles.size) { FocusRequester() }
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFocusTriggeredScroll by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.isScrollInProgress }
|
||||||
|
.collect { isScrolling ->
|
||||||
|
if (!isScrolling && !isFocusTriggeredScroll && gameTiles.isNotEmpty()) {
|
||||||
|
val firstVisible = listState.firstVisibleItemIndex
|
||||||
|
val scrollOffset = listState.firstVisibleItemScrollOffset
|
||||||
|
val layoutInfo = listState.layoutInfo
|
||||||
|
val firstVisibleItem = layoutInfo.visibleItemsInfo.firstOrNull()
|
||||||
|
val itemSize = firstVisibleItem?.size ?: 1
|
||||||
|
val newCenterIndex = if (scrollOffset > itemSize / 2) {
|
||||||
|
(firstVisible + 1).coerceAtMost(gameTiles.size - 1)
|
||||||
|
} else {
|
||||||
|
firstVisible
|
||||||
|
}
|
||||||
|
if (newCenterIndex != centerItemIndex && newCenterIndex in focusRequesters.indices) {
|
||||||
|
centerItemIndex = newCenterIndex
|
||||||
|
try {
|
||||||
|
focusRequesters[newCenterIndex].requestFocus()
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reset the flag when scroll stops
|
||||||
|
if (!isScrolling) {
|
||||||
|
isFocusTriggeredScroll = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carousel settings (matching original CarouselRecyclerView)
|
||||||
|
val borderScale = 0.6f
|
||||||
|
val borderAlpha = 0.35f
|
||||||
|
val overlapFactor = 0.15f
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val screenWidthPx = with(density) { maxWidth.toPx() }
|
||||||
|
val screenHeightPx = with(density) { maxHeight.toPx() }
|
||||||
|
|
||||||
|
val tileTextHeight = 40.dp
|
||||||
|
val tileSpacing = 6.dp
|
||||||
|
|
||||||
|
// Card size
|
||||||
|
val totalTextAreaPx = with(density) { (tileTextHeight + tileSpacing).toPx() }
|
||||||
|
val cardSizePx = (screenHeightPx - totalTextAreaPx) * 0.7f
|
||||||
|
val cardSize = with(density) { cardSizePx.toDp() }
|
||||||
|
val tileHeight = cardSize + tileSpacing + tileTextHeight
|
||||||
|
|
||||||
|
// Horizontal padding to center first/last card
|
||||||
|
val horizontalPaddingPx = (screenWidthPx - cardSizePx) / 2f
|
||||||
|
val horizontalPadding = with(density) { horizontalPaddingPx.toDp() }
|
||||||
|
|
||||||
|
// Negative spacing for overlap, no overlap currently tho
|
||||||
|
val overlapPx = cardSizePx * overlapFactor
|
||||||
|
val itemSpacing = with(density) { (-overlapPx).toDp() }
|
||||||
|
|
||||||
|
val centerPushPx = cardSizePx * 0.12f
|
||||||
|
|
||||||
|
val screenCenterPx = screenWidthPx / 2f
|
||||||
|
|
||||||
|
val itemWidthWithSpacing = cardSizePx - overlapPx
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
flingBehavior = snapFlingBehavior,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||||
|
contentPadding = PaddingValues(start = horizontalPadding, end = horizontalPadding, top = 30.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
|
||||||
|
val distanceFromCenter by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
|
||||||
|
if (itemInfo == null) {
|
||||||
|
1f
|
||||||
|
} else {
|
||||||
|
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
|
||||||
|
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
|
||||||
|
|
||||||
|
val distance = abs(itemCenterInScreen - screenCenterPx)
|
||||||
|
|
||||||
|
(distance / screenCenterPx).coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val shapedScale = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
|
||||||
|
val scale = borderScale + (1f - borderScale) * shapedScale
|
||||||
|
|
||||||
|
val shapedAlpha = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
|
||||||
|
val alpha = borderAlpha + (1f - borderAlpha) * shapedAlpha
|
||||||
|
|
||||||
|
val zIndex = 100f * (1f - distanceFromCenter)
|
||||||
|
|
||||||
|
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
|
||||||
|
val isLeftOfCenter = if (itemInfo != null) {
|
||||||
|
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
|
||||||
|
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
|
||||||
|
itemCenterInScreen < screenCenterPx
|
||||||
|
} else {
|
||||||
|
index < (gameTiles.size / 2)
|
||||||
|
}
|
||||||
|
val pushFactor = (1f - distanceFromCenter) * distanceFromCenter * 4f
|
||||||
|
val horizontalPush = centerPushPx * pushFactor * (if (isLeftOfCenter) -1f else 1f)
|
||||||
|
|
||||||
|
val tileModifier = if (index == centerItemIndex) {
|
||||||
|
Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.focusRequester(focusRequesters[index])
|
||||||
|
} else {
|
||||||
|
Modifier.focusRequester(focusRequesters[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = cardSize, height = tileHeight)
|
||||||
|
.zIndex(zIndex)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
if (focusState.hasFocus && centerItemIndex != index) {
|
||||||
|
isFocusTriggeredScroll = true
|
||||||
|
centerItemIndex = index
|
||||||
|
coroutineScope.launch {
|
||||||
|
centerOnIndex(index, animate = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onPreviewKeyEvent { keyEvent ->
|
||||||
|
if (keyEvent.type == KeyEventType.KeyDown) {
|
||||||
|
when (keyEvent.key) {
|
||||||
|
Key.ButtonX -> {
|
||||||
|
onShowGameInfo(tile)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionUp, Key.DirectionUpLeft, Key.DirectionUpRight -> {
|
||||||
|
onNavigateUp()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionLeft -> {
|
||||||
|
if (index > 0) {
|
||||||
|
focusRequesters[index - 1].requestFocus()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionRight -> {
|
||||||
|
if (index < gameTiles.size - 1) {
|
||||||
|
focusRequesters[index + 1].requestFocus()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
this.alpha = alpha
|
||||||
|
translationX = horizontalPush
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
EdenTile(
|
||||||
|
title = tile.title,
|
||||||
|
iconSize = cardSize,
|
||||||
|
tileHeight = tileHeight,
|
||||||
|
onClick = {
|
||||||
|
if (index == centerItemIndex) {
|
||||||
|
onGameClick(tile)
|
||||||
|
} else {
|
||||||
|
isFocusTriggeredScroll = true
|
||||||
|
centerItemIndex = index
|
||||||
|
coroutineScope.launch {
|
||||||
|
centerOnIndex(index, animate = true)
|
||||||
|
}
|
||||||
|
focusRequesters[index].requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
if (index == centerItemIndex) {
|
||||||
|
onGameLongClick(tile)
|
||||||
|
} else {
|
||||||
|
isFocusTriggeredScroll = true
|
||||||
|
centerItemIndex = index
|
||||||
|
coroutineScope.launch {
|
||||||
|
centerOnIndex(index, animate = true)
|
||||||
|
onGameLongClick(tile)
|
||||||
|
}
|
||||||
|
focusRequesters[index].requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = tileModifier,
|
||||||
|
imageUri = tile.iconUri,
|
||||||
|
useLargerFont = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GameGrid(
|
||||||
|
gameTiles: List<GameTile>,
|
||||||
|
onGameClick: (GameTile) -> Unit,
|
||||||
|
onGameLongClick: (GameTile) -> Unit = {},
|
||||||
|
onNavigateToSettings: () -> Unit = {},
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
onNavigateUp: () -> Unit = {},
|
||||||
|
rowCount: Int = 2,
|
||||||
|
tileIconSize: Dp = 100.dp,
|
||||||
|
paddingTop: Dp = 0.dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isLoading: Boolean = false,
|
||||||
|
emptyMessage: String? = null,
|
||||||
|
initialFocusedIndex: Int = 0,
|
||||||
|
onFocusedIndexChanged: (Int) -> Unit = {},
|
||||||
|
onShowGameInfo: (GameTile) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
|
||||||
|
when {
|
||||||
|
isLoading -> LoadingMessage()
|
||||||
|
emptyMessage != null -> EmptyMessage(emptyMessage)
|
||||||
|
else -> {
|
||||||
|
GameGridContent(
|
||||||
|
gameTiles = gameTiles,
|
||||||
|
onGameClick = onGameClick,
|
||||||
|
onGameLongClick = onGameLongClick,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
onNavigateUp = onNavigateUp,
|
||||||
|
rowCount = rowCount,
|
||||||
|
tileIconSize = tileIconSize,
|
||||||
|
paddingTop = paddingTop,
|
||||||
|
initialFocusedIndex = initialFocusedIndex,
|
||||||
|
onFocusedIndexChanged = onFocusedIndexChanged,
|
||||||
|
onXPress = { index ->
|
||||||
|
gameTiles.getOrNull(index)?.let { tile ->
|
||||||
|
onShowGameInfo(tile)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GameGridContent(
|
||||||
|
gameTiles: List<GameTile>,
|
||||||
|
onGameClick: (GameTile) -> Unit,
|
||||||
|
onGameLongClick: (GameTile) -> Unit = {},
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
onNavigateUp: () -> Unit,
|
||||||
|
rowCount: Int,
|
||||||
|
tileIconSize: Dp,
|
||||||
|
paddingTop: Dp,
|
||||||
|
initialFocusedIndex: Int = 0,
|
||||||
|
onFocusedIndexChanged: (Int) -> Unit = {},
|
||||||
|
onXPress: (Int) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
|
||||||
|
var focusedIndex by remember { mutableIntStateOf(safeInitialIndex) }
|
||||||
|
|
||||||
|
val gridState = rememberLazyGridState(
|
||||||
|
initialFirstVisibleItemIndex = safeInitialIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(gameTiles.size) {
|
||||||
|
if (focusedIndex >= gameTiles.size) {
|
||||||
|
focusedIndex = 0
|
||||||
|
gridState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val tileFocusRequesters = remember(gameTiles.size) {
|
||||||
|
List(gameTiles.size) { FocusRequester() }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(focusRequester) {
|
||||||
|
if (tileFocusRequesters.isNotEmpty()) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (tileFocusRequesters.isNotEmpty() && safeInitialIndex in tileFocusRequesters.indices) {
|
||||||
|
gridState.scrollToItem(safeInitialIndex)
|
||||||
|
tileFocusRequesters[safeInitialIndex].requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(focusedIndex) {
|
||||||
|
if (focusedIndex in tileFocusRequesters.indices) {
|
||||||
|
tileFocusRequesters[focusedIndex].requestFocus()
|
||||||
|
onFocusedIndexChanged(focusedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val outerPaddingTop = 62.dp
|
||||||
|
val outerPaddingBottom = 16.dp
|
||||||
|
val outerPaddingHorizontal = 0.dp
|
||||||
|
val gridVerticalSpacing = 0.dp
|
||||||
|
val gridVerticalPadding = 12.dp
|
||||||
|
val spacerHeight = 2.dp
|
||||||
|
val minTextHeight = 42.dp
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(
|
||||||
|
top = outerPaddingTop,
|
||||||
|
start = outerPaddingHorizontal,
|
||||||
|
end = outerPaddingHorizontal,
|
||||||
|
bottom = outerPaddingBottom
|
||||||
|
)
|
||||||
|
.onPreviewKeyEvent { keyEvent ->
|
||||||
|
if (keyEvent.type == KeyEventType.KeyDown && gameTiles.isNotEmpty()) {
|
||||||
|
val currentRow = focusedIndex % rowCount
|
||||||
|
|
||||||
|
when (keyEvent.key) {
|
||||||
|
Key.ButtonX -> {
|
||||||
|
onXPress(focusedIndex)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionRight, Key.D -> {
|
||||||
|
val nextIndex = focusedIndex + rowCount
|
||||||
|
if (nextIndex < gameTiles.size) {
|
||||||
|
focusedIndex = nextIndex
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionLeft, Key.A -> {
|
||||||
|
val prevIndex = focusedIndex - rowCount
|
||||||
|
if (prevIndex >= 0) {
|
||||||
|
focusedIndex = prevIndex
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionDown, Key.S -> {
|
||||||
|
val nextIndex = focusedIndex + 1
|
||||||
|
if (nextIndex < gameTiles.size && nextIndex % rowCount != 0) {
|
||||||
|
focusedIndex = nextIndex
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionUp, Key.W -> {
|
||||||
|
if (currentRow > 0) {
|
||||||
|
focusedIndex -= 1
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
onNavigateUp()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.TopStart
|
||||||
|
) {
|
||||||
|
val availableHeight = maxHeight
|
||||||
|
val heightForRows = (availableHeight - (gridVerticalPadding * 2) -
|
||||||
|
(gridVerticalSpacing * (rowCount - 1))).coerceAtLeast(0.dp)
|
||||||
|
val perRowHeight = heightForRows.safeDiv(rowCount)
|
||||||
|
val iconMaxSize = tileIconSize * 2
|
||||||
|
val iconSizeForGrid = (perRowHeight - spacerHeight - minTextHeight)
|
||||||
|
.coerceAtLeast(96.dp)
|
||||||
|
.coerceAtMost(iconMaxSize)
|
||||||
|
val tileHeight = perRowHeight.coerceAtLeast(iconSizeForGrid + spacerHeight + minTextHeight)
|
||||||
|
|
||||||
|
LazyHorizontalGrid(
|
||||||
|
rows = GridCells.Fixed(rowCount),
|
||||||
|
state = gridState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 24.dp, vertical = gridVerticalPadding),
|
||||||
|
) {
|
||||||
|
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
|
||||||
|
val tileModifier = if (index == focusedIndex) {
|
||||||
|
Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.focusRequester(tileFocusRequesters[index])
|
||||||
|
} else {
|
||||||
|
Modifier.focusRequester(tileFocusRequesters[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
EdenTile(
|
||||||
|
title = tile.title,
|
||||||
|
iconSize = iconSizeForGrid,
|
||||||
|
tileHeight = tileHeight,
|
||||||
|
onClick = {
|
||||||
|
focusedIndex = index
|
||||||
|
onGameClick(tile)
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
focusedIndex = index
|
||||||
|
onGameLongClick(tile)
|
||||||
|
},
|
||||||
|
modifier = tileModifier,
|
||||||
|
imageUri = tile.iconUri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoadingMessage() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
|
color = EdenTheme.colors.primary,
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = context.getString(R.string.loading),
|
||||||
|
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyMessage(message: String) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = message,
|
||||||
|
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = context.getString(R.string.manage_game_folders),
|
||||||
|
style = EdenTheme.typography.label.copy(color = EdenTheme.colors.onBackground),
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Dp.safeDiv(divisor: Int): Dp = if (divisor > 0) this / divisor else 0.dp
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
|
||||||
|
data class GameTile(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val uri: String? = null,
|
||||||
|
val iconUri: String? = null,
|
||||||
|
val programId: Long = 0L,
|
||||||
|
val developer: String = "",
|
||||||
|
val version: String = "",
|
||||||
|
val isHomebrew: Boolean = false,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromGame(game: Game): GameTile = GameTile(
|
||||||
|
id = game.path,
|
||||||
|
title = game.title,
|
||||||
|
uri = game.path,
|
||||||
|
iconUri = game.path,
|
||||||
|
programId = game.programId.toLongOrNull() ?: 0L,
|
||||||
|
developer = game.developer,
|
||||||
|
version = game.version,
|
||||||
|
isHomebrew = game.isHomebrew,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import dev.eden.emu.ui.theme.Dimens
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
|
||||||
|
enum class FooterButton(@DrawableRes val iconRes: Int) {
|
||||||
|
A(R.drawable.facebutton_a),
|
||||||
|
B(R.drawable.facebutton_b),
|
||||||
|
X(R.drawable.facebutton_x),
|
||||||
|
Y(R.drawable.facebutton_y),
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeFooter(
|
||||||
|
actions: List<FooterAction>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.paddingLg, vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingXl),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
actions.forEach { action ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = Dimens.paddingXs, vertical = 2.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = action.button.iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.9f)),
|
||||||
|
)
|
||||||
|
BasicText(action.text, style = TextStyle(Color.White.copy(0.7f), 14.sp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FooterAction(val button: FooterButton, val text: String)
|
||||||
|
data class FooterFocusRequesters(val primaryAction: FocusRequester, val secondaryAction: FocusRequester)
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import dev.eden.emu.ui.theme.Dimens
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import dev.eden.emu.ui.theme.Shapes
|
||||||
|
import dev.eden.emu.ui.utils.ConfirmKeys
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeHeader(
|
||||||
|
currentUser: String,
|
||||||
|
onUserClick: () -> Unit,
|
||||||
|
searchQuery: String,
|
||||||
|
onSearchQueryChange: (String) -> Unit,
|
||||||
|
isSearchExpanded: Boolean,
|
||||||
|
onSearchExpandedChange: (Boolean) -> Unit,
|
||||||
|
onSortClick: () -> Unit,
|
||||||
|
onFilterClick: () -> Unit,
|
||||||
|
onSettingsClick: () -> Unit,
|
||||||
|
focusRequesters: HeaderFocusRequesters,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var username by remember { mutableStateOf("Eden") }
|
||||||
|
var imagePath by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(currentUser) {
|
||||||
|
val uuid = currentUser.ifEmpty { NativeLibrary.getCurrentUser() ?: "" }
|
||||||
|
if (uuid.isNotEmpty()) {
|
||||||
|
username = NativeLibrary.getUserUsername(uuid) ?: "Eden"
|
||||||
|
imagePath = NativeLibrary.getUserImagePath(uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = Dimens.paddingXl, vertical = Dimens.paddingMd),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
UserProfileButton(username, imagePath, onUserClick, focusRequesters.userButton)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingLg),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
ExpandableSearchBar(searchQuery, onSearchQueryChange, isSearchExpanded, onSearchExpandedChange, focusRequesters.searchButton)
|
||||||
|
HeaderIconButton(R.drawable.ic_sort, context.getString(R.string.home_sort), onSortClick, focusRequesters.sortButton)
|
||||||
|
HeaderIconButton(R.drawable.ic_filter, context.getString(R.string.home_layout), onFilterClick, focusRequesters.filterButton)
|
||||||
|
HeaderIconButton(R.drawable.ic_settings, context.getString(R.string.home_settings), onSettingsClick, focusRequesters.settingsButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UserProfileButton(
|
||||||
|
username: String,
|
||||||
|
imagePath: String?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
|
||||||
|
val bitmap = remember(imagePath) {
|
||||||
|
imagePath?.takeIf { it.isNotEmpty() }?.let { path ->
|
||||||
|
File(path).takeIf { it.exists() }?.let { BitmapFactory.decodeFile(path) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultBitmap = remember {
|
||||||
|
NativeLibrary.getDefaultAccountBackupJpeg().let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.focusable(interactionSource = interactionSource)
|
||||||
|
.onPreviewKeyEvent { e ->
|
||||||
|
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
|
||||||
|
}
|
||||||
|
.clickable(interactionSource, null, onClick = onClick)
|
||||||
|
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
|
||||||
|
.border(Dimens.borderWidth, if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
|
||||||
|
.padding(horizontal = Dimens.paddingMd, vertical = 6.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingMd),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
(bitmap ?: defaultBitmap)?.let { bmp ->
|
||||||
|
Image(
|
||||||
|
bitmap = bmp.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.size(46.dp).clip(CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BasicText(username, style = EdenTheme.typography.body.copy(fontSize = 18.sp, color = Color.White))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HeaderIconButton(
|
||||||
|
iconRes: Int,
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(Dimens.iconLg)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.focusable(interactionSource = interactionSource)
|
||||||
|
.onPreviewKeyEvent { e ->
|
||||||
|
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
|
||||||
|
}
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface)
|
||||||
|
.clickable(interactionSource, null, onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(iconRes),
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(Dimens.iconSm),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HeaderFocusRequesters(
|
||||||
|
val userButton: FocusRequester,
|
||||||
|
val searchButton: FocusRequester,
|
||||||
|
val sortButton: FocusRequester,
|
||||||
|
val filterButton: FocusRequester,
|
||||||
|
val settingsButton: FocusRequester,
|
||||||
|
)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import dev.eden.emu.ui.theme.Dimens
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import dev.eden.emu.ui.theme.Shapes
|
||||||
|
import dev.eden.emu.ui.utils.DialogButton
|
||||||
|
|
||||||
|
enum class LayoutMode { TWO_ROW, CAROUSEL /*, ONE_ROW */ }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LayoutSelectionDialog(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSelectGrid: () -> Unit,
|
||||||
|
onSelectCarousel: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusRequesters = remember { List(3) { FocusRequester() } }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(Shapes.large)
|
||||||
|
.background(EdenTheme.colors.surface)
|
||||||
|
.padding(Dimens.paddingXl),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = context.getString(R.string.home_layout),
|
||||||
|
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(Dimens.paddingXl))
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text = context.getString(R.string.view_grid),
|
||||||
|
onClick = { onSelectGrid(); onDismiss() },
|
||||||
|
focusRequester = focusRequesters[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(Dimens.paddingMd))
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text = context.getString(R.string.view_carousel),
|
||||||
|
onClick = { onSelectCarousel(); onDismiss() },
|
||||||
|
focusRequester = focusRequesters[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(Dimens.paddingXl))
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text = context.getString(R.string.cancel),
|
||||||
|
onClick = onDismiss,
|
||||||
|
focusRequester = focusRequesters[2],
|
||||||
|
isSecondary = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import dev.eden.emu.ui.theme.Dimens
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import dev.eden.emu.ui.theme.Shapes
|
||||||
|
import dev.eden.emu.ui.utils.DialogButton
|
||||||
|
|
||||||
|
enum class SortMode { LAST_PLAYED, ALPHABETICAL }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortSelectionDialog(
|
||||||
|
currentSortMode: SortMode,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSelectSort: (SortMode) -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val focusRequesters = remember { List(3) { FocusRequester() } }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(Shapes.large)
|
||||||
|
.background(EdenTheme.colors.surface)
|
||||||
|
.padding(Dimens.paddingXl),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = context.getString(R.string.home_sort),
|
||||||
|
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(Dimens.paddingXl))
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text = context.getString(R.string.search_recently_played),
|
||||||
|
onClick = { onSelectSort(SortMode.LAST_PLAYED); onDismiss() },
|
||||||
|
focusRequester = focusRequesters[0],
|
||||||
|
isSelected = currentSortMode == SortMode.LAST_PLAYED,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(Dimens.paddingMd))
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text = context.getString(R.string.alphabetical),
|
||||||
|
onClick = { onSelectSort(SortMode.ALPHABETICAL); onDismiss() },
|
||||||
|
focusRequester = focusRequesters[1],
|
||||||
|
isSelected = currentSortMode == SortMode.ALPHABETICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(Dimens.paddingXl))
|
||||||
|
|
||||||
|
DialogButton(
|
||||||
|
text = context.getString(R.string.cancel),
|
||||||
|
onClick = onDismiss,
|
||||||
|
focusRequester = focusRequesters[2],
|
||||||
|
isSecondary = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.input.key.*
|
||||||
|
import dev.eden.emu.ui.components.HeaderFocusRequesters
|
||||||
|
import dev.eden.emu.ui.utils.NavKeys
|
||||||
|
|
||||||
|
/** Navigation zones for the home screen */
|
||||||
|
enum class NavigationZone { HEADER, CONTENT/*, FOOTER */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages focus and navigation between Header and Content zones.
|
||||||
|
* Uses MutableState for reactive UI updates.
|
||||||
|
* (Default Focus Manager breaks and is based on "placement", not suitable)
|
||||||
|
*/
|
||||||
|
class HomeNavigationManager(
|
||||||
|
private val headerFocusRequesters: HeaderFocusRequesters,
|
||||||
|
private val contentFocusRequester: FocusRequester,
|
||||||
|
) {
|
||||||
|
private val _currentZone: MutableState<NavigationZone> = mutableStateOf(NavigationZone.CONTENT)
|
||||||
|
val currentZone: NavigationZone get() = _currentZone.value
|
||||||
|
|
||||||
|
private val _headerIndex: MutableState<Int> = mutableIntStateOf(0)
|
||||||
|
val headerIndex: Int get() = _headerIndex.value
|
||||||
|
|
||||||
|
// Expose state for Compose observation
|
||||||
|
val currentZoneState: MutableState<NavigationZone> get() = _currentZone
|
||||||
|
val headerIndexState: MutableState<Int> get() = _headerIndex
|
||||||
|
|
||||||
|
private val headerElements = listOf(
|
||||||
|
headerFocusRequesters.userButton,
|
||||||
|
headerFocusRequesters.searchButton,
|
||||||
|
headerFocusRequesters.sortButton,
|
||||||
|
headerFocusRequesters.filterButton,
|
||||||
|
headerFocusRequesters.settingsButton,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun navigateUp() {
|
||||||
|
if (_currentZone.value == NavigationZone.CONTENT) {
|
||||||
|
_currentZone.value = NavigationZone.HEADER
|
||||||
|
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateDown() {
|
||||||
|
if (_currentZone.value == NavigationZone.HEADER) {
|
||||||
|
_currentZone.value = NavigationZone.CONTENT
|
||||||
|
contentFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateLeft() {
|
||||||
|
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value > 0) {
|
||||||
|
_headerIndex.value--
|
||||||
|
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateRight() {
|
||||||
|
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value < headerElements.lastIndex) {
|
||||||
|
_headerIndex.value++
|
||||||
|
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestInitialFocus() {
|
||||||
|
_currentZone.value = NavigationZone.CONTENT
|
||||||
|
contentFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetToContent() {
|
||||||
|
_currentZone.value = NavigationZone.CONTENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle D-Pad navigation for home screen */
|
||||||
|
fun handleHomeNavigation(keyEvent: KeyEvent, nav: HomeNavigationManager): Boolean {
|
||||||
|
if (keyEvent.type != KeyEventType.KeyDown) return false
|
||||||
|
|
||||||
|
return when (keyEvent.key) {
|
||||||
|
in NavKeys.up -> { nav.navigateUp(); true }
|
||||||
|
in NavKeys.down -> { nav.navigateDown(); true }
|
||||||
|
in NavKeys.left -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateLeft(); true } else false
|
||||||
|
in NavKeys.right -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateRight(); true } else false
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
private val LocalEdenTypography = staticCompositionLocalOf {
|
||||||
|
EdenTypography(
|
||||||
|
title = TextStyle(
|
||||||
|
fontFamily = FontFamily.SansSerif,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
),
|
||||||
|
body = TextStyle(
|
||||||
|
fontFamily = FontFamily.SansSerif,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
label = TextStyle(
|
||||||
|
fontFamily = FontFamily.SansSerif,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object EdenTheme {
|
||||||
|
val colors: EdenColors
|
||||||
|
@Composable get() = LocalEdenColors.current
|
||||||
|
|
||||||
|
val typography: EdenTypography
|
||||||
|
@Composable get() = LocalEdenTypography.current
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EdenTheme(
|
||||||
|
colors: EdenColors = LocalEdenColors.current,
|
||||||
|
typography: EdenTypography = LocalEdenTypography.current,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalEdenColors provides colors,
|
||||||
|
LocalEdenTypography provides typography,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class EdenTypography(
|
||||||
|
val title: TextStyle,
|
||||||
|
val body: TextStyle,
|
||||||
|
val label: TextStyle,
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val EdenBrandColor = Color(0xFFA161F3)
|
||||||
|
val EdenSplashColor = EdenBrandColor
|
||||||
|
val EdenGridLine = Color(0x2AA161F3)
|
||||||
|
val EdenBackground = Color(0xFF191521)
|
||||||
|
val EdenSurface = Color(0xFF221C2B)
|
||||||
|
val EdenSurfaceVariant = Color(0xFF2D2538)
|
||||||
|
val EdenOnBackground = Color(0xFFEAE5F2)
|
||||||
|
val EdenOnSurface = Color(0xFFEAE5F2)
|
||||||
|
val EdenOnSurfaceVariant = Color(0xFFB0A8BA)
|
||||||
|
val EdenOnPrimary = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
internal val LocalEdenColors = staticCompositionLocalOf {
|
||||||
|
EdenColors(
|
||||||
|
background = EdenBackground,
|
||||||
|
surface = EdenSurface,
|
||||||
|
surfaceVariant = EdenSurfaceVariant,
|
||||||
|
primary = EdenBrandColor,
|
||||||
|
onBackground = EdenOnBackground,
|
||||||
|
onSurface = EdenOnSurface,
|
||||||
|
onSurfaceVariant = EdenOnSurfaceVariant,
|
||||||
|
onPrimary = EdenOnPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class EdenColors(
|
||||||
|
val background: Color,
|
||||||
|
val surface: Color,
|
||||||
|
val surfaceVariant: Color,
|
||||||
|
val primary: Color,
|
||||||
|
val onBackground: Color,
|
||||||
|
val onSurface: Color,
|
||||||
|
val onSurfaceVariant: Color,
|
||||||
|
val onPrimary: Color,
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// Grid background
|
||||||
|
val EdenGridSpacing = 58.dp
|
||||||
|
val EdenGridLineWidth = 2.dp
|
||||||
|
const val EdenGridSpeedDpPerSecond = 12f
|
||||||
|
|
||||||
|
// Common dimensions
|
||||||
|
object Dimens {
|
||||||
|
// Padding
|
||||||
|
val paddingXs = 4.dp
|
||||||
|
val paddingSm = 8.dp
|
||||||
|
val paddingMd = 12.dp
|
||||||
|
val paddingLg = 16.dp
|
||||||
|
val paddingXl = 24.dp
|
||||||
|
|
||||||
|
// Icon sizes
|
||||||
|
val iconSm = 24.dp
|
||||||
|
val iconMd = 32.dp
|
||||||
|
val iconLg = 48.dp
|
||||||
|
|
||||||
|
// Border
|
||||||
|
val borderWidth = 2.dp
|
||||||
|
|
||||||
|
// Corner radius
|
||||||
|
val radiusSm = 8.dp
|
||||||
|
val radiusMd = 12.dp
|
||||||
|
val radiusLg = 16.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common shapes
|
||||||
|
object Shapes {
|
||||||
|
val small = RoundedCornerShape(Dimens.radiusSm)
|
||||||
|
val medium = RoundedCornerShape(Dimens.radiusMd)
|
||||||
|
val large = RoundedCornerShape(Dimens.radiusLg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.utils
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import dev.eden.emu.ui.theme.Dimens
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import dev.eden.emu.ui.theme.Shapes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New dialog button for consistent styling across dialogs
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DialogButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
isSecondary: Boolean = false,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
|
||||||
|
val backgroundColor = when {
|
||||||
|
isFocused -> EdenTheme.colors.primary
|
||||||
|
isSelected -> EdenTheme.colors.primary.copy(alpha = 0.3f)
|
||||||
|
isSecondary -> Color.Transparent
|
||||||
|
else -> EdenTheme.colors.background
|
||||||
|
}
|
||||||
|
|
||||||
|
val borderColor = when {
|
||||||
|
isFocused || isSelected -> EdenTheme.colors.primary
|
||||||
|
isSecondary -> EdenTheme.colors.onBackground.copy(alpha = 0.3f)
|
||||||
|
else -> EdenTheme.colors.onBackground.copy(alpha = 0.2f)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(Shapes.medium)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.border(Dimens.borderWidth, borderColor, Shapes.medium)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.focusable(interactionSource = interactionSource)
|
||||||
|
.onPreviewKeyEvent { event ->
|
||||||
|
if (event.type == KeyEventType.KeyUp && event.key in confirmKeys) {
|
||||||
|
onClick()
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
.clickable(interactionSource, null, onClick = onClick)
|
||||||
|
.padding(vertical = 14.dp, horizontal = 20.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = if (isSelected && !isSecondary) "✓ $text" else text,
|
||||||
|
style = EdenTheme.typography.body.copy(color = Color.White, fontSize = 16.sp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val confirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package dev.eden.emu.ui.utils
|
||||||
|
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
|
||||||
|
val ConfirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
|
||||||
|
val MenuKeys = setOf(Key.ButtonX, Key.Menu)
|
||||||
|
val BackKeys = setOf(Key.Escape, Key.Back, Key.ButtonB)
|
||||||
|
|
||||||
|
object NavKeys {
|
||||||
|
val up = setOf(Key.DirectionUp, Key.W)
|
||||||
|
val down = setOf(Key.DirectionDown, Key.S)
|
||||||
|
val left = setOf(Key.DirectionLeft, Key.A)
|
||||||
|
val right = setOf(Key.DirectionRight, Key.D)
|
||||||
|
}
|
||||||
@@ -614,4 +614,23 @@ object NativeLibrary {
|
|||||||
* Updates the device power state to global variables
|
* Updates the device power state to global variables
|
||||||
*/
|
*/
|
||||||
external fun updatePowerState(percentage: Int, isCharging: Boolean, hasBattery: Boolean)
|
external fun updatePowerState(percentage: Int, isCharging: Boolean, hasBattery: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile manager native calls
|
||||||
|
*/
|
||||||
|
external fun getAllUsers(): Array<String>?
|
||||||
|
external fun getUserUsername(uuid: String): String?
|
||||||
|
external fun getUserCount(): Long
|
||||||
|
external fun canCreateUser(): Boolean
|
||||||
|
external fun createUser(uuid: String, username: String): Boolean
|
||||||
|
external fun updateUserUsername(uuid: String, username: String): Boolean
|
||||||
|
external fun removeUser(uuid: String): Boolean
|
||||||
|
external fun getCurrentUser(): String?
|
||||||
|
external fun setCurrentUser(uuid: String): Boolean
|
||||||
|
external fun getUserImagePath(uuid: String): String?
|
||||||
|
external fun saveUserImage(uuid: String, imagePath: String): Boolean
|
||||||
|
external fun reloadProfiles()
|
||||||
|
external fun getFirmwareAvatarCount(): Int
|
||||||
|
external fun getFirmwareAvatarImage(index: Int): ByteArray?
|
||||||
|
external fun getDefaultAccountBackupJpeg(): ByteArray
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -18,6 +18,8 @@ import javax.net.ssl.TrustManagerFactory
|
|||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.LocaleList
|
import android.os.LocaleList
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.ImageLoaderFactory
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
import org.yuzu.yuzu_emu.utils.DocumentsTree
|
import org.yuzu.yuzu_emu.utils.DocumentsTree
|
||||||
@@ -28,7 +30,7 @@ import java.util.Locale
|
|||||||
|
|
||||||
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
|
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
|
||||||
|
|
||||||
class YuzuApplication : Application() {
|
class YuzuApplication : Application(), ImageLoaderFactory {
|
||||||
private fun createNotificationChannels() {
|
private fun createNotificationChannels() {
|
||||||
val name: CharSequence = getString(R.string.app_notification_channel_name)
|
val name: CharSequence = getString(R.string.app_notification_channel_name)
|
||||||
val description = getString(R.string.app_notification_channel_description)
|
val description = getString(R.string.app_notification_channel_description)
|
||||||
@@ -76,6 +78,12 @@ class YuzuApplication : Application() {
|
|||||||
createNotificationChannels()
|
createNotificationChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
return ImageLoader.Builder(this)
|
||||||
|
.crossfade(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var documentsTree: DocumentsTree? = null
|
var documentsTree: DocumentsTree? = null
|
||||||
lateinit var application: YuzuApplication
|
lateinit var application: YuzuApplication
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -649,7 +649,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
|
|||||||
|
|
||||||
fun launch(activity: AppCompatActivity, game: Game) {
|
fun launch(activity: AppCompatActivity, game: Game) {
|
||||||
val launcher = Intent(activity, EmulationActivity::class.java)
|
val launcher = Intent(activity, EmulationActivity::class.java)
|
||||||
launcher.putExtra(EXTRA_SELECTED_GAME, game)
|
launcher.putExtra("game", game) // Use "game" to match navigation argus
|
||||||
activity.startActivity(launcher)
|
activity.startActivity(launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -10,6 +10,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
|
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
|
||||||
import org.yuzu.yuzu_emu.model.Patch
|
import org.yuzu.yuzu_emu.model.Patch
|
||||||
|
import org.yuzu.yuzu_emu.model.PatchType
|
||||||
import org.yuzu.yuzu_emu.model.AddonViewModel
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||||
|
|
||||||
@@ -31,7 +32,12 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
|
|||||||
binding.addonSwitch.isChecked = model.enabled
|
binding.addonSwitch.isChecked = model.enabled
|
||||||
|
|
||||||
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
||||||
model.enabled = checked
|
if (PatchType.from(model.type) == PatchType.Update && checked) {
|
||||||
|
addonViewModel.enableOnlyThisUpdate(model)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
} else {
|
||||||
|
model.enabled = checked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val deleteAction = {
|
val deleteAction = {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ItemFirmwareAvatarBinding
|
||||||
|
|
||||||
|
class FirmwareAvatarAdapter(
|
||||||
|
private val avatars: List<Bitmap>,
|
||||||
|
private val onAvatarSelected: (Bitmap) -> Unit
|
||||||
|
) : RecyclerView.Adapter<FirmwareAvatarAdapter.AvatarViewHolder>() {
|
||||||
|
|
||||||
|
private var selectedPosition = -1
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvatarViewHolder {
|
||||||
|
val binding = ItemFirmwareAvatarBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return AvatarViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AvatarViewHolder, position: Int) {
|
||||||
|
holder.bind(avatars[position], position == selectedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = avatars.size
|
||||||
|
|
||||||
|
inner class AvatarViewHolder(
|
||||||
|
private val binding: ItemFirmwareAvatarBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(avatar: Bitmap, isSelected: Boolean) {
|
||||||
|
binding.imageAvatar.setImageBitmap(avatar)
|
||||||
|
binding.root.isChecked = isSelected
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
val previousSelected = selectedPosition
|
||||||
|
selectedPosition = bindingAdapterPosition
|
||||||
|
|
||||||
|
if (previousSelected != -1) {
|
||||||
|
notifyItemChanged(previousSelected)
|
||||||
|
}
|
||||||
|
notifyItemChanged(selectedPosition)
|
||||||
|
|
||||||
|
onAvatarSelected(avatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -7,8 +10,10 @@ import android.net.Uri
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
||||||
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
||||||
|
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||||
import org.yuzu.yuzu_emu.model.GameDir
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
|
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
|
||||||
@@ -31,6 +36,12 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
|
|||||||
path.text = Uri.parse(model.uriString).path
|
path.text = Uri.parse(model.uriString).path
|
||||||
path.marquee()
|
path.marquee()
|
||||||
|
|
||||||
|
// Set type indicator, shows below folder name, to see if DLC or Games
|
||||||
|
typeIndicator.text = when (model.type) {
|
||||||
|
DirectoryType.GAME -> activity.getString(R.string.games)
|
||||||
|
DirectoryType.EXTERNAL_CONTENT -> activity.getString(R.string.external_content)
|
||||||
|
}
|
||||||
|
|
||||||
buttonEdit.setOnClickListener {
|
buttonEdit.setOnClickListener {
|
||||||
GameFolderPropertiesDialogFragment.newInstance(model)
|
GameFolderPropertiesDialogFragment.newInstance(model)
|
||||||
.show(
|
.show(
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ListItemProfileBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.UserProfile
|
||||||
|
import java.io.File
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
|
||||||
|
class ProfileAdapter(
|
||||||
|
private val onProfileClick: (UserProfile) -> Unit,
|
||||||
|
private val onEditClick: (UserProfile) -> Unit,
|
||||||
|
private val onDeleteClick: (UserProfile) -> Unit
|
||||||
|
) : RecyclerView.Adapter<ProfileAdapter.ProfileViewHolder>() {
|
||||||
|
|
||||||
|
private var currentUserUUID: String = ""
|
||||||
|
|
||||||
|
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<UserProfile>() {
|
||||||
|
override fun areItemsTheSame(oldItem: UserProfile, newItem: UserProfile): Boolean {
|
||||||
|
return oldItem.uuid == newItem.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: UserProfile, newItem: UserProfile): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fun submitList(list: List<UserProfile>) {
|
||||||
|
differ.submitList(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentUser(uuid: String) {
|
||||||
|
currentUserUUID = uuid
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
|
||||||
|
val binding = ListItemProfileBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return ProfileViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
|
||||||
|
holder.bind(differ.currentList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = differ.currentList.size
|
||||||
|
|
||||||
|
inner class ProfileViewHolder(private val binding: ListItemProfileBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(profile: UserProfile) {
|
||||||
|
binding.textUsername.text = profile.username
|
||||||
|
binding.textUuid.text = formatUUID(profile.uuid)
|
||||||
|
|
||||||
|
val imageFile = File(profile.imagePath)
|
||||||
|
if (imageFile.exists()) {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(profile.imagePath)
|
||||||
|
binding.imageAvatar.setImageBitmap(bitmap)
|
||||||
|
} else {
|
||||||
|
val jpegData = NativeLibrary.getDefaultAccountBackupJpeg()
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
|
||||||
|
binding.imageAvatar.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.uuid == currentUserUUID) {
|
||||||
|
binding.checkContainer.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.checkContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
onProfileClick(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonEdit.setOnClickListener {
|
||||||
|
onEditClick(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonDelete.setOnClickListener {
|
||||||
|
onDeleteClick(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatUUID(uuid: String): String {
|
||||||
|
if (uuid.length != 32) return uuid
|
||||||
|
return buildString {
|
||||||
|
append(uuid.substring(0, 8))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(8, 12))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(12, 16))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(16, 20))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(20, 32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
|||||||
statusText.setTextColor(
|
statusText.setTextColor(
|
||||||
MaterialColors.getColor(
|
MaterialColors.getColor(
|
||||||
statusText,
|
statusText,
|
||||||
com.google.android.material.R.attr.colorPrimary
|
androidx.appcompat.R.attr.colorPrimary
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.features.fetcher
|
package org.yuzu.yuzu_emu.features.fetcher
|
||||||
@@ -171,7 +171,7 @@ class ReleaseAdapter(
|
|||||||
iconTint = ColorStateList.valueOf(
|
iconTint = ColorStateList.valueOf(
|
||||||
MaterialColors.getColor(
|
MaterialColors.getColor(
|
||||||
this,
|
this,
|
||||||
com.google.android.material.R.attr.colorPrimary
|
androidx.appcompat.R.attr.colorPrimary
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
-2
@@ -73,8 +73,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
|||||||
SHOW_SHADERS_BUILDING("show_shaders_building"),
|
SHOW_SHADERS_BUILDING("show_shaders_building"),
|
||||||
|
|
||||||
DEBUG_FLUSH_BY_LINE("flush_line"),
|
DEBUG_FLUSH_BY_LINE("flush_line"),
|
||||||
USE_LRU_CACHE("use_lru_cache"),
|
|
||||||
|
|
||||||
DONT_SHOW_DRIVER_SHADER_WARNING("dont_show_driver_shader_warning"),
|
DONT_SHOW_DRIVER_SHADER_WARNING("dont_show_driver_shader_warning"),
|
||||||
ENABLE_OVERLAY("enable_overlay"),
|
ENABLE_OVERLAY("enable_overlay"),
|
||||||
|
|
||||||
|
|||||||
-7
@@ -123,13 +123,6 @@ abstract class SettingsItem(
|
|||||||
// List of all general
|
// List of all general
|
||||||
val settingsItems = HashMap<String, SettingsItem>().apply {
|
val settingsItems = HashMap<String, SettingsItem>().apply {
|
||||||
put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name))
|
put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name))
|
||||||
put(
|
|
||||||
SwitchSetting(
|
|
||||||
BooleanSetting.USE_LRU_CACHE,
|
|
||||||
titleId = R.string.use_lru_cache,
|
|
||||||
descriptionId = R.string.use_lru_cache_description
|
|
||||||
)
|
|
||||||
)
|
|
||||||
put(
|
put(
|
||||||
SwitchSetting(
|
SwitchSetting(
|
||||||
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
|
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
|
||||||
|
|||||||
-1
@@ -235,7 +235,6 @@ class SettingsFragmentPresenter(
|
|||||||
|
|
||||||
add(HeaderSetting(R.string.cpu))
|
add(HeaderSetting(R.string.cpu))
|
||||||
add(IntSetting.FAST_CPU_TIME.key)
|
add(IntSetting.FAST_CPU_TIME.key)
|
||||||
add(BooleanSetting.USE_LRU_CACHE.key)
|
|
||||||
add(BooleanSetting.CORE_SYNC_CORE_SPEED.key)
|
add(BooleanSetting.CORE_SYNC_CORE_SPEED.key)
|
||||||
|
|
||||||
add(IntSetting.MEMORY_LAYOUT.key)
|
add(IntSetting.MEMORY_LAYOUT.key)
|
||||||
|
|||||||
@@ -0,0 +1,459 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.adapters.FirmwareAvatarAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentEditUserDialogBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.ProfileUtils
|
||||||
|
import org.yuzu.yuzu_emu.model.UserProfile
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import androidx.core.graphics.scale
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
class EditUserDialogFragment : Fragment() {
|
||||||
|
private var _binding: FragmentEditUserDialogBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var currentUUID: String = ""
|
||||||
|
private var isEditMode = false
|
||||||
|
private var selectedImageUri: Uri? = null
|
||||||
|
private var selectedFirmwareAvatar: Bitmap? = null
|
||||||
|
private var hasCustomImage = false
|
||||||
|
private var revertedToDefault = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_UUID = "uuid"
|
||||||
|
private const val ARG_USERNAME = "username"
|
||||||
|
|
||||||
|
fun newInstance(profile: UserProfile?): EditUserDialogFragment {
|
||||||
|
val fragment = EditUserDialogFragment()
|
||||||
|
profile?.let {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(ARG_UUID, it.uuid)
|
||||||
|
args.putString(ARG_USERNAME, it.username)
|
||||||
|
fragment.arguments = args
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val imagePickerLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
selectedImageUri = uri
|
||||||
|
loadImage(uri)
|
||||||
|
hasCustomImage = true
|
||||||
|
binding.buttonRevertImage.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentEditUserDialogBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
val existingUUID = arguments?.getString(ARG_UUID)
|
||||||
|
val existingUsername = arguments?.getString(ARG_USERNAME)
|
||||||
|
|
||||||
|
if (existingUUID != null && existingUsername != null) {
|
||||||
|
isEditMode = true
|
||||||
|
currentUUID = existingUUID
|
||||||
|
binding.toolbarNewUser.title = getString(R.string.profile_edit_user)
|
||||||
|
binding.editUsername.setText(existingUsername)
|
||||||
|
binding.textUuid.text = formatUUID(existingUUID)
|
||||||
|
binding.buttonGenerateUuid.visibility = View.GONE
|
||||||
|
|
||||||
|
val imagePath = NativeLibrary.getUserImagePath(existingUUID)
|
||||||
|
val imageFile = File(imagePath)
|
||||||
|
if (imageFile.exists()) {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imagePath)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(bitmap)
|
||||||
|
hasCustomImage = true
|
||||||
|
binding.buttonRevertImage.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
loadDefaultAvatar()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isEditMode = false
|
||||||
|
currentUUID = ProfileUtils.generateRandomUUID()
|
||||||
|
binding.toolbarNewUser.title = getString(R.string.profile_new_user)
|
||||||
|
binding.textUuid.text = formatUUID(currentUUID)
|
||||||
|
loadDefaultAvatar()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarNewUser.setNavigationOnClickListener {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.editUsername.doAfterTextChanged {
|
||||||
|
validateInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonGenerateUuid.setOnClickListener {
|
||||||
|
currentUUID = ProfileUtils.generateRandomUUID()
|
||||||
|
binding.textUuid.text = formatUUID(currentUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonSelectImage.setOnClickListener {
|
||||||
|
selectImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonRevertImage.setOnClickListener {
|
||||||
|
revertToDefaultImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeLibrary.isFirmwareAvailable()) {
|
||||||
|
binding.buttonFirmwareAvatars.visibility = View.VISIBLE
|
||||||
|
binding.buttonFirmwareAvatars.setOnClickListener {
|
||||||
|
showFirmwareAvatarPicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonSave.setOnClickListener {
|
||||||
|
saveUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonCancel.setOnClickListener {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
validateInput()
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFirmwareAvatarPicker() {
|
||||||
|
val dialogView = LayoutInflater.from(requireContext())
|
||||||
|
.inflate(R.layout.dialog_firmware_avatar_picker, null)
|
||||||
|
|
||||||
|
val gridAvatars = dialogView.findViewById<RecyclerView>(R.id.grid_avatars)
|
||||||
|
val progressLoading = dialogView.findViewById<View>(R.id.progress_loading)
|
||||||
|
val textEmpty = dialogView.findViewById<View>(R.id.text_empty)
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.profile_firmware_avatars)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val avatars = withContext(Dispatchers.IO) {
|
||||||
|
loadFirmwareAvatars()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatars.isEmpty()) {
|
||||||
|
progressLoading.visibility = View.GONE
|
||||||
|
textEmpty.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
progressLoading.visibility = View.GONE
|
||||||
|
gridAvatars.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val adapter = FirmwareAvatarAdapter(avatars) { selectedAvatar ->
|
||||||
|
val scaledBitmap = selectedAvatar.scale(256, 256)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(scaledBitmap)
|
||||||
|
selectedFirmwareAvatar = scaledBitmap
|
||||||
|
hasCustomImage = true
|
||||||
|
binding.buttonRevertImage.visibility = View.VISIBLE
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
gridAvatars.apply {
|
||||||
|
layoutManager = GridLayoutManager(requireContext(), 4)
|
||||||
|
this.adapter = adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFirmwareAvatars(): List<Bitmap> {
|
||||||
|
val avatars = mutableListOf<Bitmap>()
|
||||||
|
val count = NativeLibrary.getFirmwareAvatarCount()
|
||||||
|
|
||||||
|
for (i in 0 until count) {
|
||||||
|
try {
|
||||||
|
val imageData = NativeLibrary.getFirmwareAvatarImage(i) ?: continue
|
||||||
|
|
||||||
|
val argbData = IntArray(256 * 256)
|
||||||
|
for (pixel in 0 until 256 * 256) {
|
||||||
|
val offset = pixel * 4
|
||||||
|
val r = imageData[offset].toInt() and 0xFF
|
||||||
|
val g = imageData[offset + 1].toInt() and 0xFF
|
||||||
|
val b = imageData[offset + 2].toInt() and 0xFF
|
||||||
|
val a = imageData[offset + 3].toInt() and 0xFF
|
||||||
|
argbData[pixel] = (a shl 24) or (r shl 16) or (g shl 8) or b
|
||||||
|
}
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(argbData, 256, 256, Bitmap.Config.ARGB_8888)
|
||||||
|
avatars.add(bitmap)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatars
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatUUID(uuid: String): String {
|
||||||
|
if (uuid.length != 32) return uuid
|
||||||
|
return buildString {
|
||||||
|
append(uuid.substring(0, 8))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(8, 12))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(12, 16))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(16, 20))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(20, 32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateInput() {
|
||||||
|
val username = binding.editUsername.text.toString()
|
||||||
|
val isValid = username.isNotEmpty() && username.length <= 32
|
||||||
|
binding.buttonSave.isEnabled = isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectImage() {
|
||||||
|
val intent = Intent(Intent.ACTION_PICK).apply {
|
||||||
|
type = "image/*"
|
||||||
|
}
|
||||||
|
imagePickerLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadImage(uri: Uri) {
|
||||||
|
try {
|
||||||
|
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val source = ImageDecoder.createSource(requireContext().contentResolver, uri)
|
||||||
|
ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
|
||||||
|
decoder.setTargetSampleSize(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
MediaStore.Images.Media.getBitmap(requireContext().contentResolver, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
val croppedBitmap = centerCropBitmap(bitmap, 256, 256)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(croppedBitmap)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(getString(R.string.profile_image_load_error, e.message))
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDefaultAvatar() {
|
||||||
|
val jpegData = NativeLibrary.getDefaultAccountBackupJpeg()
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(bitmap)
|
||||||
|
|
||||||
|
hasCustomImage = false
|
||||||
|
binding.buttonRevertImage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun revertToDefaultImage() {
|
||||||
|
selectedImageUri = null
|
||||||
|
selectedFirmwareAvatar = null
|
||||||
|
revertedToDefault = true
|
||||||
|
loadDefaultAvatar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveUser() {
|
||||||
|
val username = binding.editUsername.text.toString()
|
||||||
|
|
||||||
|
if (isEditMode) {
|
||||||
|
if (NativeLibrary.updateUserUsername(currentUUID, username)) {
|
||||||
|
saveImageIfNeeded()
|
||||||
|
findNavController().popBackStack()
|
||||||
|
} else {
|
||||||
|
showError(getString(R.string.profile_update_failed))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (NativeLibrary.createUser(currentUUID, username)) {
|
||||||
|
saveImageIfNeeded()
|
||||||
|
findNavController().popBackStack()
|
||||||
|
} else {
|
||||||
|
showError(getString(R.string.profile_create_failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveImageIfNeeded() {
|
||||||
|
if (revertedToDefault && isEditMode) {
|
||||||
|
val imagePath = NativeLibrary.getUserImagePath(currentUUID)
|
||||||
|
if (imagePath != null) {
|
||||||
|
val imageFile = File(imagePath)
|
||||||
|
if (imageFile.exists()) {
|
||||||
|
imageFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCustomImage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val bitmapToSave: Bitmap? = when {
|
||||||
|
selectedFirmwareAvatar != null -> selectedFirmwareAvatar
|
||||||
|
selectedImageUri != null -> {
|
||||||
|
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val source = ImageDecoder.createSource(
|
||||||
|
requireContext().contentResolver,
|
||||||
|
selectedImageUri!!
|
||||||
|
)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
MediaStore.Images.Media.getBitmap(
|
||||||
|
requireContext().contentResolver,
|
||||||
|
selectedImageUri
|
||||||
|
)
|
||||||
|
}
|
||||||
|
centerCropBitmap(bitmap, 256, 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmapToSave == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val tempFile = File(requireContext().cacheDir, "temp_avatar_${currentUUID}.jpg")
|
||||||
|
FileOutputStream(tempFile).use { out ->
|
||||||
|
bitmapToSave.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.saveUserImage(currentUUID, tempFile.absolutePath)
|
||||||
|
|
||||||
|
tempFile.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError(getString(R.string.profile_image_save_error, e.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun centerCropBitmap(source: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
|
||||||
|
val sourceWidth = source.width
|
||||||
|
val sourceHeight = source.height
|
||||||
|
|
||||||
|
val scale = maxOf(
|
||||||
|
targetWidth.toFloat() / sourceWidth,
|
||||||
|
targetHeight.toFloat() / sourceHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
val scaledWidth = (sourceWidth * scale).toInt()
|
||||||
|
val scaledHeight = (sourceHeight * scale).toInt()
|
||||||
|
|
||||||
|
val scaledBitmap = source.scale(scaledWidth, scaledHeight)
|
||||||
|
|
||||||
|
val x = (scaledWidth - targetWidth) / 2
|
||||||
|
val y = (scaledHeight - targetHeight) / 2
|
||||||
|
|
||||||
|
return Bitmap.createBitmap(scaledBitmap, x, y, targetWidth, targetHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(message: String) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInset = barInsets.left + cutoutInsets.left
|
||||||
|
val topInset = cutoutInsets.top
|
||||||
|
val rightInset = barInsets.right + cutoutInsets.right
|
||||||
|
val bottomInset = barInsets.bottom + cutoutInsets.bottom
|
||||||
|
|
||||||
|
binding.appbar.updatePadding(
|
||||||
|
left = leftInset,
|
||||||
|
top = topInset,
|
||||||
|
right = rightInset
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.scrollContent.updatePadding(
|
||||||
|
left = leftInset,
|
||||||
|
right = rightInset
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.buttonContainer.updatePadding(
|
||||||
|
left = leftInset,
|
||||||
|
right = rightInset,
|
||||||
|
bottom = bottomInset
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1310,7 +1310,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
setTextColor(
|
setTextColor(
|
||||||
MaterialColors.getColor(
|
MaterialColors.getColor(
|
||||||
this,
|
this,
|
||||||
com.google.android.material.R.attr.colorPrimary
|
androidx.appcompat.R.attr.colorPrimary
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1497,7 +1497,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
setTextColor(
|
setTextColor(
|
||||||
MaterialColors.getColor(
|
MaterialColors.getColor(
|
||||||
this,
|
this,
|
||||||
com.google.android.material.R.attr.colorPrimary
|
androidx.appcompat.R.attr.colorPrimary
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
@@ -110,7 +110,7 @@ class FreedrenoSettingsFragment : Fragment() {
|
|||||||
val commonVars = listOf(
|
val commonVars = listOf(
|
||||||
"TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
|
"TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
|
||||||
"FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
|
"FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
|
||||||
"TU_BREADCRUMBS"
|
"TU_BREADCRUMBS", "FD_DEV_FEATURES"
|
||||||
)
|
)
|
||||||
|
|
||||||
for (varName in commonVars) {
|
for (varName in commonVars) {
|
||||||
|
|||||||
+19
-8
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -6,11 +9,13 @@ package org.yuzu.yuzu_emu.fragments
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||||
import org.yuzu.yuzu_emu.model.GameDir
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
@@ -25,14 +30,18 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|||||||
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
||||||
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
||||||
|
|
||||||
// Restore checkbox state
|
// Hide deepScan for external content, do automatically
|
||||||
binding.deepScanSwitch.isChecked =
|
if (gameDir.type == DirectoryType.EXTERNAL_CONTENT) {
|
||||||
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
binding.deepScanSwitch.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
// Restore checkbox state for game dirs
|
||||||
|
binding.deepScanSwitch.isChecked =
|
||||||
|
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
||||||
|
|
||||||
// Ensure that we can get the checkbox state even if the view is destroyed
|
|
||||||
deepScan = binding.deepScanSwitch.isChecked
|
|
||||||
binding.deepScanSwitch.setOnClickListener {
|
|
||||||
deepScan = binding.deepScanSwitch.isChecked
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
binding.deepScanSwitch.setOnClickListener {
|
||||||
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
@@ -41,8 +50,10 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
||||||
if (folderIndex != -1) {
|
if (folderIndex != -1) {
|
||||||
gamesViewModel.folders.value[folderIndex].deepScan =
|
if (gameDir.type == DirectoryType.GAME) {
|
||||||
binding.deepScanSwitch.isChecked
|
gamesViewModel.folders.value[folderIndex].deepScan =
|
||||||
|
binding.deepScanSwitch.isChecked
|
||||||
|
}
|
||||||
gamesViewModel.updateGameDirs()
|
gamesViewModel.updateGameDirs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
@@ -15,11 +15,14 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
||||||
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
@@ -73,7 +76,25 @@ class GameFoldersFragment : Fragment() {
|
|||||||
|
|
||||||
val mainActivity = requireActivity() as MainActivity
|
val mainActivity = requireActivity() as MainActivity
|
||||||
binding.buttonAdd.setOnClickListener {
|
binding.buttonAdd.setOnClickListener {
|
||||||
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
// Show a model to choose between Game and External Content
|
||||||
|
val options = arrayOf(
|
||||||
|
getString(R.string.games),
|
||||||
|
getString(R.string.external_content)
|
||||||
|
)
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.add_folders)
|
||||||
|
.setItems(options) { _, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> { // Game Folder
|
||||||
|
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||||
|
}
|
||||||
|
1 -> { // External Content Folder
|
||||||
|
mainActivity.getExternalContentDirectory.launch(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
setInsets()
|
setInsets()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
@@ -114,6 +117,17 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
add(
|
||||||
|
HomeSetting(
|
||||||
|
R.string.profile_manager,
|
||||||
|
R.string.profile_manager_description,
|
||||||
|
R.drawable.ic_account_circle,
|
||||||
|
{
|
||||||
|
binding.root.findNavController()
|
||||||
|
.navigate(R.id.action_homeSettingsFragment_to_profileManagerFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
add(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.gpu_driver_manager,
|
R.string.gpu_driver_manager,
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.adapters.ProfileAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentProfileManagerBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.UserProfile
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
|
class ProfileManagerFragment : Fragment() {
|
||||||
|
private var _binding: FragmentProfileManagerBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private lateinit var profileAdapter: ProfileAdapter
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentProfileManagerBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
binding.toolbarProfiles.setNavigationOnClickListener {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRecyclerView()
|
||||||
|
loadProfiles()
|
||||||
|
|
||||||
|
binding.buttonAddUser.setOnClickListener {
|
||||||
|
if (NativeLibrary.canCreateUser()) {
|
||||||
|
findNavController().navigate(R.id.action_profileManagerFragment_to_newUserDialog)
|
||||||
|
} else {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.profile_max_users_title)
|
||||||
|
.setMessage(R.string.profile_max_users_message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
profileAdapter = ProfileAdapter(
|
||||||
|
onProfileClick = { profile -> selectProfile(profile) },
|
||||||
|
onEditClick = { profile -> editProfile(profile) },
|
||||||
|
onDeleteClick = { profile -> confirmDeleteProfile(profile) }
|
||||||
|
)
|
||||||
|
binding.listProfiles.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
adapter = profileAdapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadProfiles() {
|
||||||
|
val profiles = mutableListOf<UserProfile>()
|
||||||
|
val userUUIDs = NativeLibrary.getAllUsers() ?: emptyArray()
|
||||||
|
val currentUserUUID = NativeLibrary.getCurrentUser()
|
||||||
|
|
||||||
|
for (uuid in userUUIDs) {
|
||||||
|
if (uuid.isNotEmpty()) {
|
||||||
|
val username = NativeLibrary.getUserUsername(uuid)
|
||||||
|
if (!username.isNullOrEmpty()) {
|
||||||
|
val imagePath = NativeLibrary.getUserImagePath(uuid) ?: ""
|
||||||
|
profiles.add(UserProfile(uuid, username, imagePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileAdapter.submitList(profiles)
|
||||||
|
profileAdapter.setCurrentUser(currentUserUUID ?: "")
|
||||||
|
|
||||||
|
binding.buttonAddUser.isEnabled = NativeLibrary.canCreateUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectProfile(profile: UserProfile) {
|
||||||
|
if (NativeLibrary.setCurrentUser(profile.uuid)) {
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun editProfile(profile: UserProfile) {
|
||||||
|
val bundle = Bundle().apply {
|
||||||
|
putString("uuid", profile.uuid)
|
||||||
|
putString("username", profile.username)
|
||||||
|
}
|
||||||
|
findNavController().navigate(R.id.action_profileManagerFragment_to_newUserDialog, bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmDeleteProfile(profile: UserProfile) {
|
||||||
|
val currentUser = NativeLibrary.getCurrentUser()
|
||||||
|
val isCurrentUser = profile.uuid == currentUser
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.profile_delete_confirm_title)
|
||||||
|
.setMessage(
|
||||||
|
if (isCurrentUser) {
|
||||||
|
getString(R.string.profile_delete_current_user_message, profile.username)
|
||||||
|
} else {
|
||||||
|
getString(R.string.profile_delete_confirm_message, profile.username)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setPositiveButton(R.string.profile_delete) { _, _ ->
|
||||||
|
deleteProfile(profile)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteProfile(profile: UserProfile) {
|
||||||
|
val currentUser = NativeLibrary.getCurrentUser()
|
||||||
|
if (!currentUser.isNullOrEmpty() && profile.uuid == currentUser) {
|
||||||
|
val users = NativeLibrary.getAllUsers() ?: emptyArray()
|
||||||
|
for (uuid in users) {
|
||||||
|
if (uuid.isNotEmpty() && uuid != profile.uuid) {
|
||||||
|
NativeLibrary.setCurrentUser(uuid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeLibrary.removeUser(profile.uuid)) {
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val fabLayoutParams = binding.buttonAddUser.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
fabLayoutParams.leftMargin = leftInsets + 24
|
||||||
|
fabLayoutParams.rightMargin = rightInsets + 24
|
||||||
|
fabLayoutParams.bottomMargin = barInsets.bottom + 24
|
||||||
|
binding.buttonAddUser.layoutParams = fabLayoutParams
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -48,16 +51,68 @@ class AddonViewModel : ViewModel() {
|
|||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
).toMutableList()
|
).toMutableList()
|
||||||
patchList.sortBy { it.name }
|
patchList.sortBy { it.name }
|
||||||
|
|
||||||
|
// Ensure only one update is enabled
|
||||||
|
ensureSingleUpdateEnabled(patchList)
|
||||||
|
|
||||||
|
removeDuplicates(patchList)
|
||||||
|
|
||||||
_patchList.value = patchList
|
_patchList.value = patchList
|
||||||
isRefreshing.set(false)
|
isRefreshing.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureSingleUpdateEnabled(patchList: MutableList<Patch>) {
|
||||||
|
val updates = patchList.filter { PatchType.from(it.type) == PatchType.Update }
|
||||||
|
if (updates.size <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val enabledUpdates = updates.filter { it.enabled }
|
||||||
|
|
||||||
|
if (enabledUpdates.size > 1) {
|
||||||
|
val nandOrSdmcEnabled = enabledUpdates.find {
|
||||||
|
it.name.contains("(NAND)") || it.name.contains("(SDMC)")
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateToKeep = nandOrSdmcEnabled ?: enabledUpdates.first()
|
||||||
|
|
||||||
|
for (patch in patchList) {
|
||||||
|
if (PatchType.from(patch.type) == PatchType.Update) {
|
||||||
|
patch.enabled = (patch === updateToKeep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeDuplicates(patchList: MutableList<Patch>) {
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
val iterator = patchList.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val patch = iterator.next()
|
||||||
|
val key = "${patch.name}|${patch.version}|${patch.type}"
|
||||||
|
if (seen.contains(key)) {
|
||||||
|
iterator.remove()
|
||||||
|
} else {
|
||||||
|
seen.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setAddonToDelete(patch: Patch?) {
|
fun setAddonToDelete(patch: Patch?) {
|
||||||
_addonToDelete.value = patch
|
_addonToDelete.value = patch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun enableOnlyThisUpdate(selectedPatch: Patch) {
|
||||||
|
val currentList = _patchList.value
|
||||||
|
for (patch in currentList) {
|
||||||
|
if (PatchType.from(patch.type) == PatchType.Update) {
|
||||||
|
patch.enabled = (patch === selectedPatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onDeleteAddon(patch: Patch) {
|
fun onDeleteAddon(patch: Patch) {
|
||||||
when (PatchType.from(patch.type)) {
|
when (PatchType.from(patch.type)) {
|
||||||
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
|
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
|
||||||
@@ -72,13 +127,27 @@ class AddonViewModel : ViewModel() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if there are multiple update versions
|
||||||
|
val updates = _patchList.value.filter { PatchType.from(it.type) == PatchType.Update }
|
||||||
|
val hasMultipleUpdates = updates.size > 1
|
||||||
|
|
||||||
NativeConfig.setDisabledAddons(
|
NativeConfig.setDisabledAddons(
|
||||||
game!!.programId,
|
game!!.programId,
|
||||||
_patchList.value.mapNotNull {
|
_patchList.value.mapNotNull {
|
||||||
if (it.enabled) {
|
if (it.enabled) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
it.name
|
if (PatchType.from(it.type) == PatchType.Update) {
|
||||||
|
if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) {
|
||||||
|
it.name
|
||||||
|
} else if (hasMultipleUpdates) {
|
||||||
|
"Update@${it.numericVersion}"
|
||||||
|
} else {
|
||||||
|
it.name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
it.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -9,5 +12,14 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class GameDir(
|
data class GameDir(
|
||||||
val uriString: String,
|
val uriString: String,
|
||||||
var deepScan: Boolean
|
var deepScan: Boolean,
|
||||||
) : Parcelable
|
val type: DirectoryType = DirectoryType.GAME
|
||||||
|
) : Parcelable {
|
||||||
|
// Needed for JNI backward compatability
|
||||||
|
constructor(uriString: String, deepScan: Boolean) : this(uriString, deepScan, DirectoryType.GAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DirectoryType {
|
||||||
|
GAME,
|
||||||
|
EXTERNAL_CONTENT
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
@@ -56,21 +56,31 @@ class GamesViewModel : ViewModel() {
|
|||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
|
||||||
getGameDirs()
|
getGameDirsAndExternalContent()
|
||||||
reloadGames(directoriesChanged = false, firstStartup = true)
|
reloadGames(directoriesChanged = false, firstStartup = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGames(games: List<Game>) {
|
fun setGames(games: List<Game>) {
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
|
||||||
val sortedList = games.sortedWith(
|
val sortedList = games.sortedWith(
|
||||||
compareBy(
|
compareByDescending<Game> { game ->
|
||||||
{ it.title.lowercase(Locale.getDefault()) },
|
preferences.getLong(game.keyLastPlayedTime, 0L)
|
||||||
{ it.path }
|
}.thenBy { it.title.lowercase(Locale.getDefault()) }
|
||||||
)
|
.thenBy { it.path }
|
||||||
)
|
)
|
||||||
|
|
||||||
_games.value = sortedList
|
_games.value = sortedList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resortGames() {
|
||||||
|
val currentGames = _games.value
|
||||||
|
if (currentGames.isNotEmpty()) {
|
||||||
|
setGames(currentGames)
|
||||||
|
_shouldScrollToTop.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setShouldSwapData(shouldSwap: Boolean) {
|
fun setShouldSwapData(shouldSwap: Boolean) {
|
||||||
_shouldSwapData.value = shouldSwap
|
_shouldSwapData.value = shouldSwap
|
||||||
}
|
}
|
||||||
@@ -144,11 +154,19 @@ class GamesViewModel : ViewModel() {
|
|||||||
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
|
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
NativeConfig.addGameDir(gameDir)
|
when (gameDir.type) {
|
||||||
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
DirectoryType.GAME -> {
|
||||||
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
|
NativeConfig.addGameDir(gameDir)
|
||||||
|
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
getGameDirs(!isFirstTimeSetup)
|
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||||
|
getGameDirsAndExternalContent(!isFirstTimeSetup)
|
||||||
|
}
|
||||||
|
DirectoryType.EXTERNAL_CONTENT -> {
|
||||||
|
addExternalContentDir(gameDir.uriString)
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
getGameDirsAndExternalContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedFromGameFragment) {
|
if (savedFromGameFragment) {
|
||||||
@@ -168,8 +186,15 @@ class GamesViewModel : ViewModel() {
|
|||||||
val removedDirIndex = gameDirs.indexOf(gameDir)
|
val removedDirIndex = gameDirs.indexOf(gameDir)
|
||||||
if (removedDirIndex != -1) {
|
if (removedDirIndex != -1) {
|
||||||
gameDirs.removeAt(removedDirIndex)
|
gameDirs.removeAt(removedDirIndex)
|
||||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
when (gameDir.type) {
|
||||||
getGameDirs()
|
DirectoryType.GAME -> {
|
||||||
|
NativeConfig.setGameDirs(gameDirs.filter { it.type == DirectoryType.GAME }.toTypedArray())
|
||||||
|
}
|
||||||
|
DirectoryType.EXTERNAL_CONTENT -> {
|
||||||
|
removeExternalContentDir(gameDir.uriString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getGameDirsAndExternalContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,15 +202,16 @@ class GamesViewModel : ViewModel() {
|
|||||||
fun updateGameDirs() =
|
fun updateGameDirs() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
NativeConfig.setGameDirs(_folders.value.toTypedArray())
|
val gameDirs = _folders.value.filter { it.type == DirectoryType.GAME }
|
||||||
getGameDirs()
|
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||||
|
getGameDirsAndExternalContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onOpenGameFoldersFragment() =
|
fun onOpenGameFoldersFragment() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
getGameDirs()
|
getGameDirsAndExternalContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,16 +219,36 @@ class GamesViewModel : ViewModel() {
|
|||||||
NativeConfig.saveGlobalConfig()
|
NativeConfig.saveGlobalConfig()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
getGameDirs(true)
|
getGameDirsAndExternalContent(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGameDirs(reloadList: Boolean = false) {
|
private fun getGameDirsAndExternalContent(reloadList: Boolean = false) {
|
||||||
val gameDirs = NativeConfig.getGameDirs()
|
val gameDirs = NativeConfig.getGameDirs().toMutableList()
|
||||||
_folders.value = gameDirs.toMutableList()
|
val externalContentDirs = NativeConfig.getExternalContentDirs().map {
|
||||||
|
GameDir(it, false, DirectoryType.EXTERNAL_CONTENT)
|
||||||
|
}
|
||||||
|
gameDirs.addAll(externalContentDirs)
|
||||||
|
_folders.value = gameDirs
|
||||||
if (reloadList) {
|
if (reloadList) {
|
||||||
reloadGames(true)
|
reloadGames(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addExternalContentDir(path: String) {
|
||||||
|
val currentDirs = NativeConfig.getExternalContentDirs().toMutableList()
|
||||||
|
if (!currentDirs.contains(path)) {
|
||||||
|
currentDirs.add(path)
|
||||||
|
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeExternalContentDir(path: String) {
|
||||||
|
val currentDirs = NativeConfig.getExternalContentDirs().toMutableList()
|
||||||
|
currentDirs.remove(path)
|
||||||
|
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -12,5 +15,6 @@ data class Patch(
|
|||||||
val version: String,
|
val version: String,
|
||||||
val type: Int,
|
val type: Int,
|
||||||
val programId: String,
|
val programId: String,
|
||||||
val titleId: String
|
val titleId: String,
|
||||||
|
val numericVersion: Long = 0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class UserProfile(
|
||||||
|
val uuid: String,
|
||||||
|
val username: String,
|
||||||
|
val imagePath: String = ""
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
object ProfileUtils {
|
||||||
|
fun generateRandomUUID(): String {
|
||||||
|
val uuid = java.util.UUID.randomUUID()
|
||||||
|
return uuid.toString().replace("-", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,565 +3,468 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.ui
|
package org.yuzu.yuzu_emu.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import android.widget.PopupMenu
|
import androidx.compose.foundation.layout.Box
|
||||||
import android.widget.Toast
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.core.view.updatePadding
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import java.util.Locale
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import kotlinx.coroutines.delay
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
|
||||||
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
|
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
|
||||||
import org.yuzu.yuzu_emu.model.AppletInfo
|
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import dev.eden.emu.ui.background.RetroGridBackground
|
||||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
import dev.eden.emu.ui.components.FooterAction
|
||||||
import org.yuzu.yuzu_emu.utils.collect
|
import dev.eden.emu.ui.components.FooterButton
|
||||||
import info.debatty.java.stringsimilarity.Jaccard
|
import dev.eden.emu.ui.components.GameCarousel
|
||||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
import dev.eden.emu.ui.components.GameGrid
|
||||||
import java.util.Locale
|
import dev.eden.emu.ui.components.GameTile
|
||||||
import androidx.core.content.edit
|
import dev.eden.emu.ui.components.HeaderFocusRequesters
|
||||||
import androidx.core.view.doOnNextLayout
|
import dev.eden.emu.ui.components.HomeFooter
|
||||||
|
import dev.eden.emu.ui.components.HomeHeader
|
||||||
|
import dev.eden.emu.ui.components.LayoutMode
|
||||||
|
import dev.eden.emu.ui.components.LayoutSelectionDialog
|
||||||
|
import dev.eden.emu.ui.components.SortMode
|
||||||
|
import dev.eden.emu.ui.components.SortSelectionDialog
|
||||||
|
import dev.eden.emu.ui.navigation.HomeNavigationManager
|
||||||
|
import dev.eden.emu.ui.navigation.NavigationZone
|
||||||
|
import dev.eden.emu.ui.navigation.handleHomeNavigation
|
||||||
|
import dev.eden.emu.ui.theme.EdenTheme
|
||||||
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
|
||||||
|
private const val PREF_LAYOUT_MODE = "home_layout_mode"
|
||||||
|
private const val PREF_SORT_MODE = "home_sort_mode"
|
||||||
|
private const val PREF_LAST_FOCUSED_INDEX = "home_last_focused_index"
|
||||||
|
private const val PREF_GAME_WAS_LAUNCHED = "home_game_was_launched"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose-based Games Fragment with Header/Footer navigation
|
||||||
|
*/
|
||||||
class GamesFragment : Fragment() {
|
class GamesFragment : Fragment() {
|
||||||
private var _binding: FragmentGamesBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private var originalHeaderTopMargin: Int? = null
|
|
||||||
private var originalHeaderBottomMargin: Int? = null
|
|
||||||
private var originalHeaderRightMargin: Int? = null
|
|
||||||
private var originalHeaderLeftMargin: Int? = null
|
|
||||||
|
|
||||||
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
|
|
||||||
private var fallbackBottomInset: Int = 0
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SEARCH_TEXT = "SearchText"
|
|
||||||
private const val PREF_SORT_TYPE = "GamesSortType"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
private lateinit var gameAdapter: GameAdapter
|
|
||||||
|
|
||||||
private val preferences =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
|
|
||||||
private lateinit var mainActivity: MainActivity
|
|
||||||
private val getGamesDirectory =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
|
||||||
if (result != null) {
|
|
||||||
mainActivity.processGamesDir(result, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCurrentViewType(): Int {
|
|
||||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
|
|
||||||
val fallback = if (isLandscape) GameAdapter.VIEW_TYPE_CAROUSEL else GameAdapter.VIEW_TYPE_GRID
|
|
||||||
return preferences.getInt(key, fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setCurrentViewType(type: Int) {
|
|
||||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
|
|
||||||
preferences.edit { putInt(key, type) }
|
|
||||||
}
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
_binding = FragmentGamesBinding.inflate(inflater)
|
return ComposeView(requireContext()).apply {
|
||||||
return binding.root
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||||
|
setContent {
|
||||||
|
EdenTheme {
|
||||||
|
HomeScreen(
|
||||||
|
gamesViewModel = gamesViewModel,
|
||||||
|
onGameLaunch = { path: String ->
|
||||||
|
val game = gamesViewModel.games.value.find { it.path == path }
|
||||||
|
if (game != null) {
|
||||||
|
// Save last played time for sorting
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
|
.edit()
|
||||||
|
.putLong(game.keyLastPlayedTime, System.currentTimeMillis())
|
||||||
|
.putBoolean(PREF_GAME_WAS_LAUNCHED, true)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
EmulationActivity.launch(
|
||||||
|
requireActivity() as androidx.appcompat.app.AppCompatActivity,
|
||||||
|
game
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateToSettings = {
|
||||||
|
findNavController().navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
|
||||||
|
},
|
||||||
|
onNavigateToUserManagement = {
|
||||||
|
findNavController().navigate(R.id.action_gamesFragment_to_profileManagerFragment)
|
||||||
|
},
|
||||||
|
onShowGameInfo = { game: Game ->
|
||||||
|
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
|
||||||
|
findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
homeViewModel.setStatusBarShadeVisibility(true)
|
homeViewModel.setStatusBarShadeVisibility(true)
|
||||||
mainActivity = requireActivity() as MainActivity
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
|
|
||||||
}
|
|
||||||
|
|
||||||
gameAdapter = GameAdapter(
|
|
||||||
requireActivity() as AppCompatActivity
|
|
||||||
)
|
|
||||||
|
|
||||||
applyGridGamesBinding()
|
|
||||||
|
|
||||||
binding.swipeRefresh.apply {
|
|
||||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setOnRefreshListener {
|
|
||||||
gamesViewModel.reloadGames(false)
|
|
||||||
}
|
|
||||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressBackgroundColorSchemeColor(
|
|
||||||
com.google.android.material.color.MaterialColors.getColor(
|
|
||||||
binding.swipeRefresh,
|
|
||||||
com.google.android.material.R.attr.colorPrimary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setColorSchemeColors(
|
|
||||||
com.google.android.material.color.MaterialColors.getColor(
|
|
||||||
binding.swipeRefresh,
|
|
||||||
com.google.android.material.R.attr.colorOnPrimary
|
|
||||||
)
|
|
||||||
)
|
|
||||||
post {
|
|
||||||
if (_binding == null) {
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = gamesViewModel.isReloading.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gamesViewModel.isReloading.collect(viewLifecycleOwner) {
|
|
||||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = it
|
|
||||||
binding.noticeText.setVisible(
|
|
||||||
visible = gamesViewModel.games.value.isEmpty() && !it,
|
|
||||||
gone = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
gamesViewModel.games.collect(viewLifecycleOwner) {
|
|
||||||
if (it.isNotEmpty()) {
|
|
||||||
setAdapter(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gamesViewModel.shouldSwapData.collect(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
resetState = { gamesViewModel.setShouldSwapData(false) }
|
|
||||||
) {
|
|
||||||
if (it) {
|
|
||||||
setAdapter(gamesViewModel.games.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gamesViewModel.shouldScrollToTop.collect(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
resetState = { gamesViewModel.setShouldScrollToTop(false) }
|
|
||||||
) { if (it) scrollToTop() }
|
|
||||||
|
|
||||||
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
|
|
||||||
if (shouldScroll) {
|
|
||||||
binding.gridGames.post {
|
|
||||||
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
|
|
||||||
gameAdapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
gamesViewModel.setShouldScrollAfterReload(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupTopView()
|
|
||||||
|
|
||||||
updateButtonsVisibility()
|
|
||||||
|
|
||||||
binding.addDirectory.setOnClickListener {
|
|
||||||
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.launchQlaunch?.setOnClickListener {
|
|
||||||
launchQLaunch()
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
val applyGridGamesBinding = {
|
|
||||||
(binding.gridGames as? RecyclerView)?.apply {
|
|
||||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
val currentViewType = getCurrentViewType()
|
|
||||||
val savedViewType = if (isLandscape || currentViewType != GameAdapter.VIEW_TYPE_CAROUSEL) currentViewType else GameAdapter.VIEW_TYPE_GRID
|
|
||||||
|
|
||||||
//This prevents Grid/List views from reusing scaled or otherwise modified ViewHolders left over from the carousel.
|
|
||||||
adapter = null
|
|
||||||
recycledViewPool.clear()
|
|
||||||
|
|
||||||
gameAdapter.setViewType(savedViewType)
|
|
||||||
currentFilter = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
|
|
||||||
|
|
||||||
// Set the correct layout manager
|
|
||||||
layoutManager = when (savedViewType) {
|
|
||||||
GameAdapter.VIEW_TYPE_GRID -> {
|
|
||||||
val columns = resources.getInteger(R.integer.game_columns_grid)
|
|
||||||
GridLayoutManager(context, columns)
|
|
||||||
}
|
|
||||||
GameAdapter.VIEW_TYPE_GRID_COMPACT -> {
|
|
||||||
val columns = resources.getInteger(R.integer.game_columns_grid)
|
|
||||||
GridLayoutManager(context, columns)
|
|
||||||
}
|
|
||||||
GameAdapter.VIEW_TYPE_LIST -> {
|
|
||||||
val columns = resources.getInteger(R.integer.game_columns_list)
|
|
||||||
GridLayoutManager(context, columns)
|
|
||||||
}
|
|
||||||
GameAdapter.VIEW_TYPE_CAROUSEL -> {
|
|
||||||
LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
|
|
||||||
}
|
|
||||||
else -> throw IllegalArgumentException("Invalid view type: $savedViewType")
|
|
||||||
}
|
|
||||||
if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
|
||||||
(binding.gridGames as? View)?.let { it -> ViewCompat.requestApplyInsets(it)}
|
|
||||||
doOnNextLayout { //Carousel: important to avoid overlap issues
|
|
||||||
(this as? CarouselRecyclerView)?.notifyLaidOut(fallbackBottomInset)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(this as? CarouselRecyclerView)?.setupCarousel(false)
|
|
||||||
}
|
|
||||||
adapter = gameAdapter
|
|
||||||
lastViewType = savedViewType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
if (_binding != null) {
|
|
||||||
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
|
||||||
gamesViewModel.lastScrollPosition = (binding.gridGames as? CarouselRecyclerView)?.getClosestChildPosition() ?: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
|
// Only resort games if a game was actually launched (not when coming back from settings)
|
||||||
(binding.gridGames as? CarouselRecyclerView)?.setupCarousel(true)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
(binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition)
|
if (prefs.getBoolean(PREF_GAME_WAS_LAUNCHED, false)) {
|
||||||
|
prefs.edit().putBoolean(PREF_GAME_WAS_LAUNCHED, false).apply()
|
||||||
|
gamesViewModel.resortGames()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HomeScreen(
|
||||||
|
gamesViewModel: GamesViewModel,
|
||||||
|
onGameLaunch: (String) -> Unit,
|
||||||
|
onNavigateToSettings: () -> Unit,
|
||||||
|
onNavigateToUserManagement: () -> Unit,
|
||||||
|
onShowGameInfo: (Game) -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val games by gamesViewModel.games.collectAsState()
|
||||||
|
val isLoading by gamesViewModel.isReloading.collectAsState()
|
||||||
|
val shouldScrollToTop by gamesViewModel.shouldScrollToTop.collectAsState()
|
||||||
|
|
||||||
|
val preferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||||
|
val savedLayoutMode = remember {
|
||||||
|
val savedValue = preferences.getString(PREF_LAYOUT_MODE, LayoutMode.CAROUSEL.name)
|
||||||
|
try {
|
||||||
|
LayoutMode.valueOf(savedValue ?: LayoutMode.CAROUSEL.name)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
LayoutMode.CAROUSEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout mode state (Grid or Carousel)
|
||||||
|
var layoutMode by remember { mutableStateOf(savedLayoutMode) }
|
||||||
|
var showLayoutDialog by remember { mutableStateOf(false) }
|
||||||
|
var showSortDialog by remember { mutableStateOf(false) }
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
var isSearchExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Game properties screen state
|
||||||
|
var selectedGameTile by remember { mutableStateOf<GameTile?>(null) }
|
||||||
|
|
||||||
|
// Sort mode state
|
||||||
|
val savedSortMode = remember {
|
||||||
|
val savedValue = preferences.getString(PREF_SORT_MODE, SortMode.LAST_PLAYED.name)
|
||||||
|
try {
|
||||||
|
SortMode.valueOf(savedValue ?: SortMode.LAST_PLAYED.name)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
SortMode.LAST_PLAYED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sortMode by remember { mutableStateOf(savedSortMode) }
|
||||||
|
|
||||||
|
// Save sort mode when it changes
|
||||||
|
LaunchedEffect(sortMode) {
|
||||||
|
preferences.edit().putString(PREF_SORT_MODE, sortMode.name).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save layout mode when it changes
|
||||||
|
LaunchedEffect(layoutMode) {
|
||||||
|
preferences.edit().putString(PREF_LAYOUT_MODE, layoutMode.name).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember last focused index across layout changes - load from preferences
|
||||||
|
val savedFocusedIndex = remember {
|
||||||
|
preferences.getInt(PREF_LAST_FOCUSED_INDEX, 0)
|
||||||
|
}
|
||||||
|
var lastFocusedIndex by remember { mutableStateOf(savedFocusedIndex) }
|
||||||
|
|
||||||
|
LaunchedEffect(lastFocusedIndex) {
|
||||||
|
preferences.edit().putInt(PREF_LAST_FOCUSED_INDEX, lastFocusedIndex).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
var listVersion by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(sortMode) {
|
||||||
|
lastFocusedIndex = 0
|
||||||
|
listVersion++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset to first item when games are resorted (e.g. after playing a game)
|
||||||
|
// Else the sorting only takes effect after relaunch
|
||||||
|
LaunchedEffect(shouldScrollToTop) {
|
||||||
|
if (shouldScrollToTop) {
|
||||||
|
lastFocusedIndex = 0
|
||||||
|
listVersion++ // Force complete recomposition
|
||||||
|
gamesViewModel.setShouldScrollToTop(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current user UUID
|
||||||
|
var currentUserUuid by remember { mutableStateOf("") }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
currentUserUuid = NativeLibrary.getCurrentUser() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val headerFocusRequesters = remember {
|
||||||
|
HeaderFocusRequesters(
|
||||||
|
userButton = FocusRequester(),
|
||||||
|
searchButton = FocusRequester(),
|
||||||
|
sortButton = FocusRequester(),
|
||||||
|
filterButton = FocusRequester(),
|
||||||
|
settingsButton = FocusRequester(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Don't recreate on layoutMode change - this causes zones state loss
|
||||||
|
val contentFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
val navigationManager = remember {
|
||||||
|
HomeNavigationManager(
|
||||||
|
headerFocusRequesters = headerFocusRequesters,
|
||||||
|
contentFocusRequester = contentFocusRequester,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request initial focus
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(200)
|
||||||
|
navigationManager.requestInitialFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and sort games based on search query and sort mode
|
||||||
|
val filteredGames = remember(games, searchQuery, sortMode) {
|
||||||
|
val filtered = if (searchQuery.isBlank()) {
|
||||||
|
games
|
||||||
|
} else {
|
||||||
|
games.filter { game ->
|
||||||
|
game.title.lowercase(Locale.getDefault())
|
||||||
|
.contains(searchQuery.lowercase(Locale.getDefault()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
when (sortMode) {
|
||||||
|
SortMode.LAST_PLAYED -> {
|
||||||
|
filtered.sortedByDescending { game ->
|
||||||
|
preferences.getLong(game.keyLastPlayedTime, 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SortMode.ALPHABETICAL -> {
|
||||||
|
filtered.sortedBy { it.title.lowercase(Locale.getDefault()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val gameTiles = remember(filteredGames) {
|
||||||
|
filteredGames.map { game ->
|
||||||
|
GameTile.fromGame(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO key handling at top level
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
RetroGridBackground()
|
||||||
|
|
||||||
|
val contentOffset = 24.dp
|
||||||
|
key(listVersion, layoutMode) {
|
||||||
|
when (layoutMode) {
|
||||||
|
LayoutMode.TWO_ROW -> {
|
||||||
|
GameGrid(
|
||||||
|
gameTiles = gameTiles,
|
||||||
|
onGameClick = { tile: GameTile ->
|
||||||
|
val path = tile.uri ?: return@GameGrid
|
||||||
|
onGameLaunch(path)
|
||||||
|
},
|
||||||
|
onGameLongClick = { tile: GameTile ->
|
||||||
|
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||||
|
if (game != null) {
|
||||||
|
onShowGameInfo(game)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNavigateToSettings = {},
|
||||||
|
focusRequester = contentFocusRequester,
|
||||||
|
onNavigateUp = {
|
||||||
|
navigationManager.navigateUp()
|
||||||
|
},
|
||||||
|
rowCount = 2,
|
||||||
|
tileIconSize = 120.dp,
|
||||||
|
isLoading = isLoading,
|
||||||
|
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
|
||||||
|
initialFocusedIndex = lastFocusedIndex,
|
||||||
|
onFocusedIndexChanged = { newIndex ->
|
||||||
|
lastFocusedIndex = newIndex
|
||||||
|
},
|
||||||
|
onShowGameInfo = { tile: GameTile ->
|
||||||
|
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||||
|
if (game != null) {
|
||||||
|
onShowGameInfo(game)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = contentOffset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutMode.CAROUSEL -> {
|
||||||
|
GameCarousel(
|
||||||
|
gameTiles = gameTiles,
|
||||||
|
onGameClick = { tile: GameTile ->
|
||||||
|
val path = tile.uri ?: return@GameCarousel
|
||||||
|
onGameLaunch(path)
|
||||||
|
},
|
||||||
|
onGameLongClick = { tile: GameTile ->
|
||||||
|
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||||
|
if (game != null) {
|
||||||
|
onShowGameInfo(game)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusRequester = contentFocusRequester,
|
||||||
|
onNavigateUp = {
|
||||||
|
navigationManager.navigateUp()
|
||||||
|
},
|
||||||
|
initialFocusedIndex = lastFocusedIndex,
|
||||||
|
onFocusedIndexChanged = { newIndex ->
|
||||||
|
lastFocusedIndex = newIndex
|
||||||
|
},
|
||||||
|
onShowGameInfo = { tile: GameTile ->
|
||||||
|
val game = filteredGames.firstOrNull { it.path == tile.uri }
|
||||||
|
if (game != null) {
|
||||||
|
onShowGameInfo(game)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = contentOffset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Default to Grid
|
||||||
|
GameGrid(
|
||||||
|
gameTiles = gameTiles,
|
||||||
|
onGameClick = { tile: GameTile ->
|
||||||
|
val path = tile.uri ?: return@GameGrid
|
||||||
|
onGameLaunch(path)
|
||||||
|
},
|
||||||
|
onNavigateToSettings = {},
|
||||||
|
focusRequester = contentFocusRequester,
|
||||||
|
onNavigateUp = {
|
||||||
|
navigationManager.navigateUp()
|
||||||
|
},
|
||||||
|
rowCount = 2,
|
||||||
|
tileIconSize = 120.dp,
|
||||||
|
isLoading = isLoading,
|
||||||
|
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = contentOffset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refocus content when layout mode changes
|
||||||
|
LaunchedEffect(layoutMode) {
|
||||||
|
delay(200)
|
||||||
|
contentFocusRequester.requestFocus()
|
||||||
|
navigationManager.resetToContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refocus content when sort mode changes
|
||||||
|
LaunchedEffect(sortMode) {
|
||||||
|
delay(200)
|
||||||
|
contentFocusRequester.requestFocus()
|
||||||
|
navigationManager.resetToContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.onPreviewKeyEvent { keyEvent ->
|
||||||
|
if (navigationManager.currentZone == NavigationZone.HEADER) {
|
||||||
|
handleHomeNavigation(keyEvent, navigationManager)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
HomeHeader(
|
||||||
|
currentUser = currentUserUuid,
|
||||||
|
onUserClick = onNavigateToUserManagement,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
onSearchQueryChange = { query -> searchQuery = query },
|
||||||
|
isSearchExpanded = isSearchExpanded,
|
||||||
|
onSearchExpandedChange = { expanded -> isSearchExpanded = expanded },
|
||||||
|
onSortClick = {
|
||||||
|
showSortDialog = true
|
||||||
|
},
|
||||||
|
onFilterClick = {
|
||||||
|
showLayoutDialog = true
|
||||||
|
},
|
||||||
|
onSettingsClick = onNavigateToSettings,
|
||||||
|
focusRequesters = headerFocusRequesters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentZone by navigationManager.currentZoneState
|
||||||
|
val headerIdx by navigationManager.headerIndexState
|
||||||
|
val footerActions = when (currentZone) {
|
||||||
|
NavigationZone.HEADER -> {
|
||||||
|
val headerText = when (headerIdx) {
|
||||||
|
0 -> context.getString(R.string.footer_open) // User profile
|
||||||
|
1 -> context.getString(R.string.home_search) // Search
|
||||||
|
else -> context.getString(R.string.footer_open) // Sort, Filter, Settings
|
||||||
|
}
|
||||||
|
listOf(FooterAction(FooterButton.A, headerText))
|
||||||
|
}
|
||||||
|
NavigationZone.CONTENT -> {
|
||||||
|
listOf(
|
||||||
|
FooterAction(FooterButton.A, context.getString(R.string.home_start)),
|
||||||
|
FooterAction(FooterButton.X, context.getString(R.string.footer_game_info)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HomeFooter(
|
||||||
|
actions = footerActions,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout selection dialog
|
||||||
|
if (showLayoutDialog) {
|
||||||
|
LayoutSelectionDialog(
|
||||||
|
onDismiss = { showLayoutDialog = false },
|
||||||
|
onSelectGrid = { layoutMode = LayoutMode.TWO_ROW },
|
||||||
|
onSelectCarousel = { layoutMode = LayoutMode.CAROUSEL },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort selection dialog
|
||||||
|
if (showSortDialog) {
|
||||||
|
SortSelectionDialog(
|
||||||
|
currentSortMode = sortMode,
|
||||||
|
onDismiss = { showSortDialog = false },
|
||||||
|
onSelectSort = { selectedSortMode ->
|
||||||
|
sortMode = selectedSortMode
|
||||||
|
gamesViewModel.resortGames()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var lastSearchText: String = ""
|
|
||||||
private var lastFilter: Int = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
|
|
||||||
|
|
||||||
private fun setAdapter(games: List<Game>) {
|
|
||||||
val currentSearchText = binding.searchText.text.toString()
|
|
||||||
val currentFilter = binding.filterButton.id
|
|
||||||
|
|
||||||
val searchChanged = currentSearchText != lastSearchText
|
|
||||||
val filterChanged = currentFilter != lastFilter
|
|
||||||
|
|
||||||
if (searchChanged || filterChanged) {
|
|
||||||
filterAndSearch(games)
|
|
||||||
lastSearchText = currentSearchText
|
|
||||||
lastFilter = currentFilter
|
|
||||||
} else {
|
|
||||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
|
|
||||||
gamesViewModel.setFilteredGames(games)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupTopView() {
|
|
||||||
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
|
|
||||||
if (text.toString().isNotEmpty()) {
|
|
||||||
binding.clearButton.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.clearButton.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
filterAndSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
|
|
||||||
binding.searchBackground.setOnClickListener { focusSearch() }
|
|
||||||
|
|
||||||
// Setup view button
|
|
||||||
binding.viewButton.setOnClickListener { showViewMenu(it) }
|
|
||||||
|
|
||||||
// Setup filter button
|
|
||||||
binding.filterButton.setOnClickListener { view ->
|
|
||||||
showFilterMenu(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup settings button
|
|
||||||
binding.settingsButton.setOnClickListener { navigateToSettings() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToSettings() {
|
|
||||||
val navController = findNavController()
|
|
||||||
navController.navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showViewMenu(anchor: View) {
|
|
||||||
val popup = PopupMenu(requireContext(), anchor)
|
|
||||||
popup.menuInflater.inflate(R.menu.menu_game_views, popup.menu)
|
|
||||||
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
if (!isLandscape) {
|
|
||||||
popup.menu.findItem(R.id.view_carousel)?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentViewType = getCurrentViewType()
|
|
||||||
when (currentViewType) {
|
|
||||||
GameAdapter.VIEW_TYPE_LIST -> popup.menu.findItem(R.id.view_list).isChecked = true
|
|
||||||
GameAdapter.VIEW_TYPE_GRID_COMPACT -> popup.menu.findItem(R.id.view_grid_compact).isChecked = true
|
|
||||||
GameAdapter.VIEW_TYPE_GRID -> popup.menu.findItem(R.id.view_grid).isChecked = true
|
|
||||||
GameAdapter.VIEW_TYPE_CAROUSEL -> popup.menu.findItem(R.id.view_carousel).isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.view_grid -> {
|
|
||||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
|
||||||
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID)
|
|
||||||
applyGridGamesBinding()
|
|
||||||
item.isChecked = true
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.view_grid_compact -> {
|
|
||||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
|
||||||
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID_COMPACT)
|
|
||||||
applyGridGamesBinding()
|
|
||||||
item.isChecked = true
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.view_list -> {
|
|
||||||
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
|
|
||||||
setCurrentViewType(GameAdapter.VIEW_TYPE_LIST)
|
|
||||||
applyGridGamesBinding()
|
|
||||||
item.isChecked = true
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.view_carousel -> {
|
|
||||||
if (!item.isChecked || getCurrentViewType() != GameAdapter.VIEW_TYPE_CAROUSEL) {
|
|
||||||
setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL)
|
|
||||||
applyGridGamesBinding()
|
|
||||||
item.isChecked = true
|
|
||||||
onResume()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFilterMenu(anchor: View) {
|
|
||||||
val popup = PopupMenu(requireContext(), anchor)
|
|
||||||
popup.menuInflater.inflate(R.menu.menu_game_filters, popup.menu)
|
|
||||||
|
|
||||||
// Set checked state based on current filter
|
|
||||||
when (currentFilter) {
|
|
||||||
R.id.alphabetical -> popup.menu.findItem(R.id.alphabetical).isChecked = true
|
|
||||||
R.id.filter_recently_played -> popup.menu.findItem(R.id.filter_recently_played).isChecked =
|
|
||||||
true
|
|
||||||
|
|
||||||
R.id.filter_recently_added -> popup.menu.findItem(R.id.filter_recently_added).isChecked =
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
|
||||||
currentFilter = item.itemId
|
|
||||||
preferences.edit { putInt(PREF_SORT_TYPE, currentFilter) }
|
|
||||||
filterAndSearch()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track current filter
|
|
||||||
private var currentFilter = View.NO_ID
|
|
||||||
|
|
||||||
private fun filterAndSearch(baseList: List<Game> = gamesViewModel.games.value) {
|
|
||||||
val filteredList: List<Game> = when (currentFilter) {
|
|
||||||
R.id.alphabetical -> baseList.sortedBy { it.title }
|
|
||||||
R.id.filter_recently_played -> {
|
|
||||||
baseList.filter {
|
|
||||||
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
|
|
||||||
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
|
||||||
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
|
|
||||||
}
|
|
||||||
R.id.filter_recently_added -> {
|
|
||||||
baseList.filter {
|
|
||||||
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
|
|
||||||
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
|
||||||
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
|
|
||||||
}
|
|
||||||
else -> baseList
|
|
||||||
}
|
|
||||||
|
|
||||||
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
|
||||||
if (searchTerm.isEmpty()) {
|
|
||||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(
|
|
||||||
filteredList
|
|
||||||
)
|
|
||||||
gamesViewModel.setFilteredGames(filteredList)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
|
|
||||||
val sortedList = filteredList.mapNotNull { game ->
|
|
||||||
val title = game.title.lowercase(Locale.getDefault())
|
|
||||||
val score = searchAlgorithm.similarity(searchTerm, title)
|
|
||||||
if (score > 0.03) {
|
|
||||||
ScoredGame(score, game)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.sortedByDescending { it.score }.map { it.item }
|
|
||||||
|
|
||||||
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList)
|
|
||||||
gamesViewModel.setFilteredGames(sortedList)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ScoredGame(val score: Double, val item: Game)
|
|
||||||
|
|
||||||
private fun focusSearch() {
|
|
||||||
binding.searchText.requestFocus()
|
|
||||||
val imm = requireActivity()
|
|
||||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
|
||||||
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollToTop() {
|
|
||||||
if (_binding != null) {
|
|
||||||
(binding.gridGames as? CarouselRecyclerView)?.smoothScrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchQLaunch() {
|
|
||||||
try {
|
|
||||||
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.QLaunch.entryId)
|
|
||||||
if (appletPath.isEmpty()) {
|
|
||||||
Toast.makeText(
|
|
||||||
requireContext(),
|
|
||||||
R.string.applets_error_applet,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.setCurrentAppletId(AppletInfo.QLaunch.appletId)
|
|
||||||
|
|
||||||
val qlaunchGame = Game(
|
|
||||||
title = getString(R.string.qlaunch_applet),
|
|
||||||
path = appletPath
|
|
||||||
)
|
|
||||||
|
|
||||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(qlaunchGame)
|
|
||||||
findNavController().navigate(action)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(
|
|
||||||
requireContext(),
|
|
||||||
"Failed to launch QLaunch: ${e.message}",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateButtonsVisibility() {
|
|
||||||
val showQLaunch = BooleanSetting.ENABLE_QLAUNCH_BUTTON.getBoolean()
|
|
||||||
val showFolder = BooleanSetting.ENABLE_FOLDER_BUTTON.getBoolean()
|
|
||||||
val isFirmwareAvailable = NativeLibrary.isFirmwareAvailable()
|
|
||||||
|
|
||||||
val shouldShowQLaunch = showQLaunch && isFirmwareAvailable
|
|
||||||
binding.launchQlaunch.visibility = if (shouldShowQLaunch) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
binding.addDirectory.visibility = if (showFolder) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
|
||||||
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
|
|
||||||
|
|
||||||
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressViewEndTarget(
|
|
||||||
false,
|
|
||||||
barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
|
|
||||||
)
|
|
||||||
|
|
||||||
val leftInset = barInsets.left + cutoutInsets.left
|
|
||||||
val rightInset = barInsets.right + cutoutInsets.right
|
|
||||||
val topInset = maxOf(barInsets.top, cutoutInsets.top)
|
|
||||||
|
|
||||||
val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
mlpSwipe.leftMargin = leftInset
|
|
||||||
mlpSwipe.rightMargin = rightInset
|
|
||||||
binding.swipeRefresh.layoutParams = mlpSwipe
|
|
||||||
|
|
||||||
val mlpHeader = binding.header.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
|
|
||||||
// Store original margins only once
|
|
||||||
if (originalHeaderTopMargin == null) {
|
|
||||||
originalHeaderTopMargin = mlpHeader.topMargin
|
|
||||||
originalHeaderRightMargin = mlpHeader.rightMargin
|
|
||||||
originalHeaderLeftMargin = mlpHeader.leftMargin
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always set margin as original + insets
|
|
||||||
mlpHeader.leftMargin = (originalHeaderLeftMargin ?: 0) + leftInset
|
|
||||||
mlpHeader.rightMargin = (originalHeaderRightMargin ?: 0) + rightInset
|
|
||||||
mlpHeader.topMargin = (originalHeaderTopMargin ?: 0) + topInset + resources.getDimensionPixelSize(
|
|
||||||
R.dimen.spacing_med
|
|
||||||
)
|
|
||||||
binding.header.layoutParams = mlpHeader
|
|
||||||
|
|
||||||
binding.noticeText.updatePadding(bottom = spacingNavigation)
|
|
||||||
|
|
||||||
binding.gridGames.updatePadding(
|
|
||||||
top = resources.getDimensionPixelSize(R.dimen.spacing_med)
|
|
||||||
)
|
|
||||||
|
|
||||||
val mlpFab = binding.addDirectory.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
|
|
||||||
mlpFab.leftMargin = leftInset + fabPadding
|
|
||||||
mlpFab.bottomMargin = barInsets.bottom + fabPadding
|
|
||||||
mlpFab.rightMargin = rightInset + fabPadding
|
|
||||||
binding.addDirectory.layoutParams = mlpFab
|
|
||||||
|
|
||||||
binding.launchQlaunch?.let { qlaunchButton ->
|
|
||||||
val mlpQLaunch = qlaunchButton.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
mlpQLaunch.leftMargin = leftInset + fabPadding
|
|
||||||
mlpQLaunch.bottomMargin = barInsets.bottom + fabPadding
|
|
||||||
qlaunchButton.layoutParams = mlpQLaunch
|
|
||||||
}
|
|
||||||
|
|
||||||
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
|
||||||
val gestureInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
|
|
||||||
val bottomInset = maxOf(navInsets.bottom, gestureInsets.bottom, cutoutInsets.bottom)
|
|
||||||
fallbackBottomInset = bottomInset
|
|
||||||
(binding.gridGames as? CarouselRecyclerView)?.notifyInsetsReady(bottomInset)
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ThemeProvider {
|
class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
@@ -123,6 +124,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||||
|
|
||||||
|
// Hide status bar and navigation bar for fullscreen experience
|
||||||
|
// Maybe a setting? Reminds me: SafeArea Testing!
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
hide(WindowInsetsCompat.Type.statusBars())
|
||||||
|
hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
|
||||||
window.statusBarColor =
|
window.statusBarColor =
|
||||||
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
||||||
window.navigationBarColor =
|
window.navigationBarColor =
|
||||||
@@ -199,8 +208,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
var artifact = tag
|
var artifact = tag
|
||||||
// Nightly builds have a slightly different format
|
// Nightly builds have a slightly different format
|
||||||
if (NativeLibrary.isNightlyBuild()) {
|
if (NativeLibrary.isNightlyBuild()) {
|
||||||
val splitTag = tag.split('.')
|
artifact = tag.substringAfter('.', tag)
|
||||||
artifact = splitTag.subList(1, splitTag.size - 1).joinToString(".")
|
|
||||||
}
|
}
|
||||||
downloadAndInstallUpdate(tag, artifact)
|
downloadAndInstallUpdate(tag, artifact)
|
||||||
}
|
}
|
||||||
@@ -389,6 +397,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val getExternalContentDirectory =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
|
if (result != null) {
|
||||||
|
processExternalContentDir(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
|
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
|
||||||
contentResolver.takePersistableUriPermission(
|
contentResolver.takePersistableUriPermission(
|
||||||
result,
|
result,
|
||||||
@@ -410,6 +425,27 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun processExternalContentDir(result: Uri) {
|
||||||
|
contentResolver.takePersistableUriPermission(
|
||||||
|
result,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
val uriString = result.toString()
|
||||||
|
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
||||||
|
if (folder != null) {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.folder_already_added,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val externalContentDir = org.yuzu.yuzu_emu.model.GameDir(uriString, false, org.yuzu.yuzu_emu.model.DirectoryType.EXTERNAL_CONTENT)
|
||||||
|
gamesViewModel.addFolder(externalContentDir, savedFromGameFragment = false)
|
||||||
|
}
|
||||||
|
|
||||||
val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
processKey(result, "keys")
|
processKey(result, "keys")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
@@ -25,6 +25,7 @@ object DirectoryInitialization {
|
|||||||
initializeInternalStorage()
|
initializeInternalStorage()
|
||||||
NativeLibrary.initializeSystem(false)
|
NativeLibrary.initializeSystem(false)
|
||||||
NativeConfig.initializeGlobalConfig()
|
NativeConfig.initializeGlobalConfig()
|
||||||
|
NativeLibrary.reloadProfiles()
|
||||||
migrateSettings()
|
migrateSettings()
|
||||||
areDirectoriesReady = true
|
areDirectoriesReady = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -49,6 +49,17 @@ object GameHelper {
|
|||||||
// Remove previous filesystem provider information so we can get up to date version info
|
// Remove previous filesystem provider information so we can get up to date version info
|
||||||
NativeLibrary.clearFilesystemProvider()
|
NativeLibrary.clearFilesystemProvider()
|
||||||
|
|
||||||
|
// Scan External Content directories and register all NSP/XCI files
|
||||||
|
val externalContentDirs = NativeConfig.getExternalContentDirs()
|
||||||
|
for (externalDir in externalContentDirs) {
|
||||||
|
if (externalDir.isNotEmpty()) {
|
||||||
|
val externalDirUri = externalDir.toUri()
|
||||||
|
if (FileUtil.isTreeUriValid(externalDirUri)) {
|
||||||
|
scanExternalContentRecursive(FileUtil.listFiles(externalDirUri), 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val badDirs = mutableListOf<Int>()
|
val badDirs = mutableListOf<Int>()
|
||||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||||
val gameDirUri = gameDir.uriString.toUri()
|
val gameDirUri = gameDir.uriString.toUri()
|
||||||
@@ -88,6 +99,33 @@ object GameHelper {
|
|||||||
return games.toList()
|
return games.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File extensions considered as external content, buuut should
|
||||||
|
// be done better imo.
|
||||||
|
private val externalContentExtensions = setOf("nsp", "xci")
|
||||||
|
|
||||||
|
private fun scanExternalContentRecursive(
|
||||||
|
files: Array<MinimalDocumentFile>,
|
||||||
|
depth: Int
|
||||||
|
) {
|
||||||
|
if (depth <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach {
|
||||||
|
if (it.isDirectory) {
|
||||||
|
scanExternalContentRecursive(
|
||||||
|
FileUtil.listFiles(it.uri),
|
||||||
|
depth - 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||||
|
if (externalContentExtensions.contains(extension)) {
|
||||||
|
NativeLibrary.addFileToFilesystemProvider(it.uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun addGamesRecursive(
|
private fun addGamesRecursive(
|
||||||
games: MutableList<Game>,
|
games: MutableList<Game>,
|
||||||
files: Array<MinimalDocumentFile>,
|
files: Array<MinimalDocumentFile>,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -25,31 +28,29 @@ import org.yuzu.yuzu_emu.R
|
|||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
|
||||||
class GameIconFetcher(
|
class GameObjectIconFetcher(
|
||||||
private val game: Game,
|
private val game: Game,
|
||||||
private val options: Options
|
private val options: Options
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
|
val bitmap = decodeGameIcon(game.path)
|
||||||
|
?: throw IllegalStateException("Failed to decode game icon for: ${game.title}")
|
||||||
|
|
||||||
return DrawableResult(
|
return DrawableResult(
|
||||||
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
|
drawable = bitmap.toDrawable(options.context.resources),
|
||||||
isSampled = false,
|
isSampled = false,
|
||||||
dataSource = DataSource.DISK
|
dataSource = DataSource.DISK
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
private fun decodeGameIcon(path: String): Bitmap? {
|
||||||
val data = GameMetadata.getIcon(uri)
|
val iconBytes = GameMetadata.getIcon(path)
|
||||||
return BitmapFactory.decodeByteArray(
|
return BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
|
||||||
data,
|
|
||||||
0,
|
|
||||||
data.size,
|
|
||||||
BitmapFactory.Options()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory : Fetcher.Factory<Game> {
|
class Factory : Fetcher.Factory<Game> {
|
||||||
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
|
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
|
||||||
GameIconFetcher(data, options)
|
GameObjectIconFetcher(data, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ object GameIconUtils {
|
|||||||
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
|
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
|
||||||
.components {
|
.components {
|
||||||
add(GameIconKeyer())
|
add(GameIconKeyer())
|
||||||
add(GameIconFetcher.Factory())
|
add(GameObjectIconFetcher.Factory())
|
||||||
}
|
}
|
||||||
.memoryCache {
|
.memoryCache {
|
||||||
MemoryCache.Builder(YuzuApplication.appContext)
|
MemoryCache.Builder(YuzuApplication.appContext)
|
||||||
@@ -99,9 +100,6 @@ object GameIconUtils {
|
|||||||
R.id.shortcut_foreground,
|
R.id.shortcut_foreground,
|
||||||
getGameIcon(lifecycleOwner, game).toDrawable(YuzuApplication.appContext.resources)
|
getGameIcon(lifecycleOwner, game).toDrawable(YuzuApplication.appContext.resources)
|
||||||
)
|
)
|
||||||
val inset = YuzuApplication.appContext.resources
|
|
||||||
.getDimensionPixelSize(R.dimen.icon_inset)
|
|
||||||
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
|
|
||||||
return IconCompat.createWithAdaptiveBitmap(
|
return IconCompat.createWithAdaptiveBitmap(
|
||||||
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
|
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -204,4 +204,12 @@ object NativeConfig {
|
|||||||
external fun getSdmcDir(): String
|
external fun getSdmcDir(): String
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun setSdmcDir(path: String)
|
external fun setSdmcDir(path: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External Content Provider
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun getExternalContentDirs(): Array<String>
|
||||||
|
@Synchronized
|
||||||
|
external fun setExternalContentDirs(dirs: Array<String>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
@@ -144,6 +144,15 @@ object FreedrenoPresets {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val DEV_FEATURES_UBWC_HINT = FreedrenoPreset(
|
||||||
|
name = "Dev - UBWC Flag Hint",
|
||||||
|
description = "Enable TP UBWC flag hint for development",
|
||||||
|
icon = "ic_dev_features",
|
||||||
|
variables = mapOf(
|
||||||
|
"FD_DEV_FEATURES" to "enable_tp_ubwc_flag_hint=1"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val PERFORMANCE_DEFAULT = FreedrenoPreset(
|
val PERFORMANCE_DEFAULT = FreedrenoPreset(
|
||||||
name = "Performance - Default",
|
name = "Performance - Default",
|
||||||
description = "Clear all debug options for performance",
|
description = "Clear all debug options for performance",
|
||||||
@@ -159,6 +168,7 @@ object FreedrenoPresets {
|
|||||||
CAPTURE_FRAMES,
|
CAPTURE_FRAMES,
|
||||||
SHADER_DEBUG,
|
SHADER_DEBUG,
|
||||||
GPU_HANG_TRACE,
|
GPU_HANG_TRACE,
|
||||||
|
DEV_FEATURES_UBWC_HINT,
|
||||||
PERFORMANCE_DEFAULT
|
PERFORMANCE_DEFAULT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -74,7 +74,7 @@ class GradientBorderCardView @JvmOverloads constructor(
|
|||||||
borderPaint.shader = null
|
borderPaint.shader = null
|
||||||
val typedValue = android.util.TypedValue()
|
val typedValue = android.util.TypedValue()
|
||||||
context.theme.resolveAttribute(
|
context.theme.resolveAttribute(
|
||||||
com.google.android.material.R.attr.colorPrimary,
|
androidx.appcompat.R.attr.colorPrimary,
|
||||||
typedValue,
|
typedValue,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <common/fs/path_util.h>
|
#include <common/fs/path_util.h>
|
||||||
#include <common/logging/log.h>
|
#include <common/logging/log.h>
|
||||||
|
#include <common/settings.h>
|
||||||
#include <input_common/main.h>
|
#include <input_common/main.h>
|
||||||
#include "android_config.h"
|
#include "android_config.h"
|
||||||
#include "android_settings.h"
|
#include "android_settings.h"
|
||||||
@@ -69,6 +70,18 @@ void AndroidConfig::ReadPathValues() {
|
|||||||
}
|
}
|
||||||
EndArray();
|
EndArray();
|
||||||
|
|
||||||
|
// Read external content directories
|
||||||
|
Settings::values.external_content_dirs.clear();
|
||||||
|
const int external_dirs_size = BeginArray(std::string("external_content_dirs"));
|
||||||
|
for (int i = 0; i < external_dirs_size; ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
std::string dir_path = ReadStringSetting(std::string("path"));
|
||||||
|
if (!dir_path.empty()) {
|
||||||
|
Settings::values.external_content_dirs.push_back(dir_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
const auto nand_dir_setting = ReadStringSetting(std::string("nand_directory"));
|
const auto nand_dir_setting = ReadStringSetting(std::string("nand_directory"));
|
||||||
if (!nand_dir_setting.empty()) {
|
if (!nand_dir_setting.empty()) {
|
||||||
Common::FS::SetEdenPath(Common::FS::EdenPath::NANDDir, nand_dir_setting);
|
Common::FS::SetEdenPath(Common::FS::EdenPath::NANDDir, nand_dir_setting);
|
||||||
@@ -241,6 +254,14 @@ void AndroidConfig::SavePathValues() {
|
|||||||
}
|
}
|
||||||
EndArray();
|
EndArray();
|
||||||
|
|
||||||
|
// Save external content directories
|
||||||
|
BeginArray(std::string("external_content_dirs"));
|
||||||
|
for (size_t i = 0; i < Settings::values.external_content_dirs.size(); ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
WriteStringSetting(std::string("path"), Settings::values.external_content_dirs[i]);
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
// Save custom NAND directory
|
// Save custom NAND directory
|
||||||
const auto nand_path = Common::FS::GetEdenPathString(Common::FS::EdenPath::NANDDir);
|
const auto nand_path = Common::FS::GetEdenPathString(Common::FS::EdenPath::NANDDir);
|
||||||
WriteStringSetting(std::string("nand_directory"), nand_path,
|
WriteStringSetting(std::string("nand_directory"), nand_path,
|
||||||
|
|||||||
@@ -54,10 +54,6 @@ namespace AndroidSettings {
|
|||||||
Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
|
Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
|
||||||
Settings::Category::GpuDriver};
|
Settings::Category::GpuDriver};
|
||||||
|
|
||||||
// LRU Cache
|
|
||||||
Settings::SwitchableSetting<bool> use_lru_cache{linkage, true, "use_lru_cache",
|
|
||||||
Settings::Category::System};
|
|
||||||
|
|
||||||
Settings::Setting<s32> theme{linkage, 0, "theme", Settings::Category::Android};
|
Settings::Setting<s32> theme{linkage, 0, "theme", Settings::Category::Android};
|
||||||
Settings::Setting<s32> theme_mode{linkage, -1, "theme_mode", Settings::Category::Android};
|
Settings::Setting<s32> theme_mode{linkage, -1, "theme_mode", Settings::Category::Android};
|
||||||
Settings::Setting<bool> black_backgrounds{linkage, false, "black_backgrounds",
|
Settings::Setting<bool> black_backgrounds{linkage, false, "black_backgrounds",
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ struct RomMetadata {
|
|||||||
bool isHomebrew;
|
bool isHomebrew;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
|
ankerl::unordered_dense::map<std::string, RomMetadata> m_rom_metadata_cache;
|
||||||
|
|
||||||
RomMetadata CacheRomMetadata(const std::string& path) {
|
RomMetadata CacheRomMetadata(const std::string& path) {
|
||||||
const auto file =
|
const auto file =
|
||||||
|
|||||||
@@ -49,12 +49,17 @@
|
|||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "frontend_common/play_time_manager.h"
|
#include "frontend_common/play_time_manager.h"
|
||||||
|
#include "core/constants.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/cpu_manager.h"
|
#include "core/cpu_manager.h"
|
||||||
#include "core/crypto/key_manager.h"
|
#include "core/crypto/key_manager.h"
|
||||||
#include "core/file_sys/card_image.h"
|
#include "core/file_sys/card_image.h"
|
||||||
#include "core/file_sys/content_archive.h"
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/control_metadata.h"
|
||||||
#include "core/file_sys/fs_filesystem.h"
|
#include "core/file_sys/fs_filesystem.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
#include "core/file_sys/submission_package.h"
|
#include "core/file_sys/submission_package.h"
|
||||||
#include "core/file_sys/vfs/vfs.h"
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
#include "core/file_sys/vfs/vfs_real.h"
|
#include "core/file_sys/vfs/vfs_real.h"
|
||||||
@@ -210,6 +215,109 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto extension = Common::ToLower(filepath.substr(filepath.find_last_of('.') + 1));
|
||||||
|
|
||||||
|
if (extension == "nsp") {
|
||||||
|
auto nsp = std::make_shared<FileSys::NSP>(file);
|
||||||
|
if (nsp->GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
std::map<u64, u32> nsp_versions;
|
||||||
|
std::map<u64, std::string> nsp_version_strings;
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : nsp->GetNCAs()) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (content_type == FileSys::ContentRecordType::Meta) {
|
||||||
|
const auto meta_nca = std::make_shared<FileSys::NCA>(nca->GetBaseFile());
|
||||||
|
if (meta_nca->GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
const auto section0 = meta_nca->GetSubdirectories();
|
||||||
|
if (!section0.empty()) {
|
||||||
|
for (const auto& meta_file : section0[0]->GetFiles()) {
|
||||||
|
if (meta_file->GetExtension() == "cnmt") {
|
||||||
|
FileSys::CNMT cnmt(meta_file);
|
||||||
|
nsp_versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content_type == FileSys::ContentRecordType::Control &&
|
||||||
|
title_type == FileSys::TitleType::Update) {
|
||||||
|
auto romfs = nca->GetRomFS();
|
||||||
|
if (romfs) {
|
||||||
|
auto extracted = FileSys::ExtractRomFS(romfs);
|
||||||
|
if (extracted) {
|
||||||
|
auto nacp_file = extracted->GetFile("control.nacp");
|
||||||
|
if (!nacp_file) {
|
||||||
|
nacp_file = extracted->GetFile("Control.nacp");
|
||||||
|
}
|
||||||
|
if (nacp_file) {
|
||||||
|
FileSys::NACP nacp(nacp_file);
|
||||||
|
auto ver_str = nacp.GetVersionString();
|
||||||
|
if (!ver_str.empty()) {
|
||||||
|
nsp_version_strings[title_id] = ver_str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : nsp->GetNCAs()) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (title_type == FileSys::TitleType::Update) {
|
||||||
|
u32 version = 0;
|
||||||
|
auto ver_it = nsp_versions.find(title_id);
|
||||||
|
if (ver_it != nsp_versions.end()) {
|
||||||
|
version = ver_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string version_string;
|
||||||
|
auto str_it = nsp_version_strings.find(title_id);
|
||||||
|
if (str_it != nsp_version_strings.end()) {
|
||||||
|
version_string = str_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_manual_provider->AddEntryWithVersion(
|
||||||
|
title_type, content_type, title_id, version, version_string,
|
||||||
|
nca->GetBaseFile());
|
||||||
|
|
||||||
|
LOG_DEBUG(Frontend, "Added NSP update entry - TitleID: {:016X}, Version: {}, VersionStr: {}",
|
||||||
|
title_id, version, version_string);
|
||||||
|
} else {
|
||||||
|
// Use regular AddEntry for non-updates
|
||||||
|
m_manual_provider->AddEntry(title_type, content_type, title_id,
|
||||||
|
nca->GetBaseFile());
|
||||||
|
LOG_DEBUG(Frontend, "Added NSP entry - TitleID: {:016X}, TitleType: {}, ContentType: {}",
|
||||||
|
title_id, static_cast<int>(title_type), static_cast<int>(content_type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle XCI files
|
||||||
|
if (extension == "xci") {
|
||||||
|
FileSys::XCI xci{file};
|
||||||
|
if (xci.GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
const auto nsp = xci.GetSecurePartitionNSP();
|
||||||
|
if (nsp) {
|
||||||
|
for (const auto& title : nsp->GetNCAs()) {
|
||||||
|
for (const auto& entry : title.second) {
|
||||||
|
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
||||||
|
entry.second->GetBaseFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto loader = Loader::GetLoader(m_system, file);
|
auto loader = Loader::GetLoader(m_system, file);
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
return;
|
return;
|
||||||
@@ -226,17 +334,6 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
|||||||
m_manual_provider->AddEntry(FileSys::TitleType::Application,
|
m_manual_provider->AddEntry(FileSys::TitleType::Application,
|
||||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||||
program_id, file);
|
program_id, file);
|
||||||
} else if (res2 == Loader::ResultStatus::Success &&
|
|
||||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
|
||||||
const auto nsp = file_type == Loader::FileType::NSP
|
|
||||||
? std::make_shared<FileSys::NSP>(file)
|
|
||||||
: FileSys::XCI{file}.GetSecurePartitionNSP();
|
|
||||||
for (const auto& title : nsp->GetNCAs()) {
|
|
||||||
for (const auto& entry : title.second) {
|
|
||||||
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
|
||||||
entry.second->GetBaseFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1331,7 +1428,8 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
|
|||||||
Common::Android::ToJString(env, patch.name),
|
Common::Android::ToJString(env, patch.name),
|
||||||
Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type),
|
Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type),
|
||||||
Common::Android::ToJString(env, std::to_string(patch.program_id)),
|
Common::Android::ToJString(env, std::to_string(patch.program_id)),
|
||||||
Common::Android::ToJString(env, std::to_string(patch.title_id)));
|
Common::Android::ToJString(env, std::to_string(patch.title_id)),
|
||||||
|
static_cast<jlong>(patch.numeric_version));
|
||||||
env->SetObjectArrayElement(jpatchArray, i, jpatch);
|
env->SetObjectArrayElement(jpatchArray, i, jpatch);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
@@ -1678,7 +1776,7 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", version_str, variant);
|
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant);
|
||||||
const std::string url = fmt::format("{}/{}/releases/download/{}/{}",
|
const std::string url = fmt::format("{}/{}/releases/download/{}/{}",
|
||||||
std::string{Common::g_build_auto_update_website},
|
std::string{Common::g_build_auto_update_website},
|
||||||
std::string{Common::g_build_auto_update_repo},
|
std::string{Common::g_build_auto_update_repo},
|
||||||
@@ -1686,6 +1784,7 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
|
|||||||
apk_filename);
|
apk_filename);
|
||||||
|
|
||||||
env->ReleaseStringUTFChars(tag, version_str);
|
env->ReleaseStringUTFChars(tag, version_str);
|
||||||
|
env->ReleaseStringUTFChars(artifact, artifact_str);
|
||||||
env->ReleaseStringUTFChars(packageId, package_id_str);
|
env->ReleaseStringUTFChars(packageId, package_id_str);
|
||||||
return env->NewStringUTF(url.c_str());
|
return env->NewStringUTF(url.c_str());
|
||||||
}
|
}
|
||||||
@@ -1697,4 +1796,388 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion(
|
|||||||
return env->NewStringUTF(Common::g_build_version);
|
return env->NewStringUTF(Common::g_build_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getAllUsers(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
|
||||||
|
manager.ResetUserSaveFile();
|
||||||
|
|
||||||
|
if (manager.GetUserCount() == 0) {
|
||||||
|
manager.CreateNewUser(Common::UUID::MakeRandom(), "Eden");
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& users = manager.GetAllUsers();
|
||||||
|
|
||||||
|
jclass string_class = env->FindClass("java/lang/String");
|
||||||
|
if (!string_class) {
|
||||||
|
return env->NewObjectArray(0, env->FindClass("java/lang/Object"), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsize valid_count = 0;
|
||||||
|
for (const auto& user : users) {
|
||||||
|
if (user.IsValid()) {
|
||||||
|
valid_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobjectArray result = env->NewObjectArray(valid_count, string_class, nullptr);
|
||||||
|
if (!result) {
|
||||||
|
return env->NewObjectArray(0, string_class, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill array sequentially with only valid users
|
||||||
|
jsize array_index = 0;
|
||||||
|
for (const auto& user : users) {
|
||||||
|
if (user.IsValid()) {
|
||||||
|
jstring uuid_str = env->NewStringUTF(user.FormattedString().c_str());
|
||||||
|
if (uuid_str) {
|
||||||
|
env->SetObjectArrayElement(result, array_index++, uuid_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserUsername(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
Service::Account::ProfileBase profile{};
|
||||||
|
if (!manager.GetProfileBase(uuid, profile)) {
|
||||||
|
jstring result = env->NewStringUTF("");
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto text = Common::StringFromFixedZeroTerminatedBuffer(
|
||||||
|
reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
|
||||||
|
jstring result = env->NewStringUTF(text.c_str());
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserCount(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
return static_cast<jlong>(manager.GetUserCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_canCreateUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
return manager.CanSystemRegisterUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_createUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid,
|
||||||
|
jstring jusername) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto username = Common::Android::GetJString(env, jusername);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto result = manager.CreateNewUser(uuid, username);
|
||||||
|
if (result.IsSuccess()) {
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_updateUserUsername(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid,
|
||||||
|
jstring jusername) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto username = Common::Android::GetJString(env, jusername);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
Service::Account::ProfileBase profile{};
|
||||||
|
if (!manager.GetProfileBase(uuid, profile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fill(profile.username.begin(), profile.username.end(), '\0');
|
||||||
|
std::copy(username.begin(), username.end(), profile.username.begin());
|
||||||
|
|
||||||
|
if (manager.SetProfileBase(uuid, profile)) {
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto user_index = manager.GetUserIndex(uuid);
|
||||||
|
if (!user_index) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings::values.current_user.GetValue() == static_cast<s32>(*user_index)) {
|
||||||
|
Settings::values.current_user = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager.RemoveUser(uuid)) {
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
manager.ResetUserSaveFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getCurrentUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto user_id = manager.GetUser(Settings::values.current_user.GetValue());
|
||||||
|
if (!user_id) {
|
||||||
|
jstring result = env->NewStringUTF("");
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
jstring result = env->NewStringUTF(user_id->FormattedString().c_str());
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto index = manager.GetUserIndex(uuid);
|
||||||
|
if (index) {
|
||||||
|
Settings::values.current_user = static_cast<s32>(*index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserImagePath(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) /
|
||||||
|
fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormattedString());
|
||||||
|
|
||||||
|
jstring result = Common::Android::ToJString(env, Common::FS::PathToUTF8String(path));
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_saveUserImage(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid,
|
||||||
|
jstring jimagePath) {
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
const auto image_source = Common::Android::GetJString(env, jimagePath);
|
||||||
|
|
||||||
|
const auto dest_path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) /
|
||||||
|
fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormattedString());
|
||||||
|
|
||||||
|
const auto dest_dir = dest_path.parent_path();
|
||||||
|
if (!Common::FS::CreateDirs(dest_dir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::filesystem::copy_file(image_source, dest_path,
|
||||||
|
std::filesystem::copy_options::overwrite_existing);
|
||||||
|
return true;
|
||||||
|
} catch (const std::filesystem::filesystem_error& e) {
|
||||||
|
LOG_ERROR(Common_Filesystem, "Failed to copy image file: {}", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadProfiles(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
manager.ResetUserSaveFile();
|
||||||
|
|
||||||
|
// create a default user if non exist
|
||||||
|
if (manager.GetUserCount() == 0) {
|
||||||
|
manager.CreateNewUser(Common::UUID::MakeRandom(), "Eden");
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Service_ACC, "Profile manager reloaded, user count: {}", manager.GetUserCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// for firmware avatar images
|
||||||
|
static std::vector<uint8_t> DecompressYaz0(const FileSys::VirtualFile& file) {
|
||||||
|
if (!file) {
|
||||||
|
return std::vector<uint8_t>();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t magic{};
|
||||||
|
file->ReadObject(&magic, 0);
|
||||||
|
if (magic != Common::MakeMagic('Y', 'a', 'z', '0')) {
|
||||||
|
return std::vector<uint8_t>();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t decoded_length{};
|
||||||
|
file->ReadObject(&decoded_length, 4);
|
||||||
|
decoded_length = Common::swap32(decoded_length);
|
||||||
|
|
||||||
|
std::size_t input_size = file->GetSize() - 16;
|
||||||
|
std::vector<uint8_t> input(input_size);
|
||||||
|
file->ReadBytes(input.data(), input_size, 16);
|
||||||
|
|
||||||
|
uint32_t input_offset{};
|
||||||
|
uint32_t output_offset{};
|
||||||
|
std::vector<uint8_t> output(decoded_length);
|
||||||
|
|
||||||
|
uint16_t mask{};
|
||||||
|
uint8_t header{};
|
||||||
|
|
||||||
|
while (output_offset < decoded_length) {
|
||||||
|
if ((mask >>= 1) == 0) {
|
||||||
|
if (input_offset >= input.size()) break;
|
||||||
|
header = input[input_offset++];
|
||||||
|
mask = 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((header & mask) != 0) {
|
||||||
|
if (output_offset >= output.size() || input_offset >= input.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
output[output_offset++] = input[input_offset++];
|
||||||
|
} else {
|
||||||
|
if (input_offset + 1 >= input.size()) break;
|
||||||
|
uint8_t byte1 = input[input_offset++];
|
||||||
|
uint8_t byte2 = input[input_offset++];
|
||||||
|
|
||||||
|
uint32_t dist = ((byte1 & 0xF) << 8) | byte2;
|
||||||
|
uint32_t position = output_offset - (dist + 1);
|
||||||
|
|
||||||
|
uint32_t length = byte1 >> 4;
|
||||||
|
if (length == 0) {
|
||||||
|
if (input_offset >= input.size()) break;
|
||||||
|
length = static_cast<uint32_t>(input[input_offset++]) + 0x12;
|
||||||
|
} else {
|
||||||
|
length += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < length && output_offset < decoded_length; ++i) {
|
||||||
|
output[output_offset++] = output[position++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FileSys::VirtualDir GetFirmwareAvatarDirectory() {
|
||||||
|
constexpr u64 AvatarImageDataId = 0x010000000000080AULL;
|
||||||
|
|
||||||
|
auto* bis_system = EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
|
||||||
|
if (!bis_system) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto nca = bis_system->GetEntry(AvatarImageDataId, FileSys::ContentRecordType::Data);
|
||||||
|
if (!nca) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto romfs = nca->GetRomFS();
|
||||||
|
if (!romfs) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto extracted = FileSys::ExtractRomFS(romfs);
|
||||||
|
if (!extracted) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extracted->GetSubdirectory("chara");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getFirmwareAvatarCount(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
const auto chara_dir = GetFirmwareAvatarDirectory();
|
||||||
|
if (!chara_dir) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& item : chara_dir->GetFiles()) {
|
||||||
|
if (item->GetExtension() == "szs") {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getFirmwareAvatarImage(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jint index) {
|
||||||
|
const auto chara_dir = GetFirmwareAvatarDirectory();
|
||||||
|
if (!chara_dir) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int current_index = 0;
|
||||||
|
for (const auto& item : chara_dir->GetFiles()) {
|
||||||
|
if (item->GetExtension() != "szs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_index == index) {
|
||||||
|
auto image_data = DecompressYaz0(item);
|
||||||
|
if (image_data.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
jbyteArray result = env->NewByteArray(image_data.size());
|
||||||
|
if (result) {
|
||||||
|
env->SetByteArrayRegion(result, 0, image_data.size(),
|
||||||
|
reinterpret_cast<const jbyte*>(image_data.data()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
current_index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultAccountBackupJpeg(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
jbyteArray result = env->NewByteArray(Core::Constants::ACCOUNT_BACKUP_JPEG.size());
|
||||||
|
if (result) {
|
||||||
|
env->SetByteArrayRegion(result, 0, Core::Constants::ACCOUNT_BACKUP_JPEG.size(),
|
||||||
|
reinterpret_cast<const jbyte*>(Core::Constants::ACCOUNT_BACKUP_JPEG.data()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -583,4 +583,26 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setSdmcDir(JNIEnv* env, jobject
|
|||||||
Common::FS::SetEdenPath(Common::FS::EdenPath::SDMCDir, path);
|
Common::FS::SetEdenPath(Common::FS::EdenPath::SDMCDir, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getExternalContentDirs(JNIEnv* env,
|
||||||
|
jobject obj) {
|
||||||
|
const auto& dirs = Settings::values.external_content_dirs;
|
||||||
|
jobjectArray jdirsArray =
|
||||||
|
env->NewObjectArray(dirs.size(), Common::Android::GetStringClass(),
|
||||||
|
Common::Android::ToJString(env, ""));
|
||||||
|
for (size_t i = 0; i < dirs.size(); ++i) {
|
||||||
|
env->SetObjectArrayElement(jdirsArray, i, Common::Android::ToJString(env, dirs[i]));
|
||||||
|
}
|
||||||
|
return jdirsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setExternalContentDirs(JNIEnv* env, jobject obj,
|
||||||
|
jobjectArray jdirs) {
|
||||||
|
Settings::values.external_content_dirs.clear();
|
||||||
|
const int size = env->GetArrayLength(jdirs);
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
auto jdir = static_cast<jstring>(env->GetObjectArrayElement(jdirs, i));
|
||||||
|
Settings::values.external_content_dirs.push_back(Common::Android::GetJString(env, jdir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,6 +258,11 @@ Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);
|
LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);
|
||||||
|
|
||||||
|
if (var_name == "FD_DEV_FEATURES") {
|
||||||
|
LOG_INFO(Frontend, "[Freedreno] FD_DEV_FEATURES enabled: {}", value);
|
||||||
|
}
|
||||||
|
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@
|
|||||||
#include "input_common/drivers/virtual_gamepad.h"
|
#include "input_common/drivers/virtual_gamepad.h"
|
||||||
#include "native.h"
|
#include "native.h"
|
||||||
|
|
||||||
std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
|
ankerl::unordered_dense::map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
|
||||||
|
|
||||||
bool IsHandheldOnly() {
|
bool IsHandheldOnly() {
|
||||||
const auto npad_style_set =
|
const auto npad_style_set =
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3s-3,-1.34 -3,-3S10.34,5 12,5zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22c0.03,-1.99 4,-3.08 6,-3.08c1.99,0 5.97,1.09 6,3.08C16.71,17.92 14.5,19.2 12,19.2z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zm0,7h12v-2H3v2z"/>
|
||||||
|
</vector>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="vertical"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:layout_gravity="center_vertical">
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
@@ -23,12 +23,25 @@
|
|||||||
android:layout_gravity="center_vertical|start"
|
android:layout_gravity="center_vertical|start"
|
||||||
android:requiresFadingEdge="horizontal"
|
android:requiresFadingEdge="horizontal"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toTopOf="@+id/type_indicator"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="@string/select_gpu_driver_default" />
|
tools:text="@string/select_gpu_driver_default" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/type_indicator"
|
||||||
|
style="@style/TextAppearance.Material3.LabelSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/path"
|
||||||
|
tools:text="Games" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_layout"
|
android:id="@+id/button_layout"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="300dp">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/grid_avatars"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_empty"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/profile_firmware_avatars_unavailable"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_new_user"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:title="@string/profile_new_user"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:titleCentered="false" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/scroll_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="88dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:paddingVertical="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="128dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="64dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_user_avatar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/profile_avatar"
|
||||||
|
tools:src="@drawable/ic_account_circle" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_select_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/profile_select_image"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:icon="@drawable/ic_add"
|
||||||
|
android:layout_marginEnd="4dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_firmware_avatars"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/profile_firmware_avatars"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:icon="@drawable/ic_account_circle"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_revert_image"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/profile_revert_image"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:hint="@string/profile_username">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/edit_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLength="32"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:minHeight="48dp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
|
app:cardCornerRadius="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/profile_uuid"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_uuid"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
tools:text="12345678-1234-1234-1234-123456789012" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_generate_uuid"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/profile_generate"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
app:icon="@drawable/ic_refresh"
|
||||||
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/profile_uuid_description"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/button_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="0dp"
|
||||||
|
app:cardElevation="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:id="@+id/button_container"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:paddingBottom="24dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@android:string/cancel"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_save"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/save"
|
||||||
|
style="@style/Widget.Material3.Button" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_profiles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:title="@string/profile_manager"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:titleCentered="false" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_profiles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="96dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
tools:listitem="@layout/list_item_profile" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/button_add_user"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
android:text="@string/profile_add_user"
|
||||||
|
android:contentDescription="@string/profile_add_user"
|
||||||
|
app:icon="@drawable/ic_add"
|
||||||
|
app:iconGravity="start"
|
||||||
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:checkable="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_avatar"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/profile_avatar" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginVertical="6dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/avatar_container"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="28dp"
|
||||||
|
app:cardElevation="1dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_avatar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@drawable/ic_account_circle"
|
||||||
|
android:contentDescription="@string/profile_avatar" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/check_container"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
|
app:cardBackgroundColor="?attr/colorPrimary"
|
||||||
|
app:cardCornerRadius="10dp"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/avatar_container"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/avatar_container"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon_check"
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/ic_check"
|
||||||
|
app:tint="?attr/colorOnPrimary"
|
||||||
|
android:contentDescription="@string/profile_current_user" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatar_container"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_edit"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/avatar_container"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/avatar_container">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="User Name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_uuid"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
tools:text="12345678-1234-1234-1234-123456789012" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_edit"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
app:icon="@drawable/ic_edit"
|
||||||
|
app:iconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_delete"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:contentDescription="@string/profile_edit" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
app:icon="@drawable/ic_delete"
|
||||||
|
app:iconTint="?attr/colorError"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:contentDescription="@string/profile_delete" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/gamesFragment"
|
android:id="@+id/gamesFragment"
|
||||||
android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
|
android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
|
||||||
android:label="PlatformGamesFragment" />
|
android:label="PlatformGamesFragment">
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_gamesFragment_to_homeSettingsFragment"
|
android:id="@+id/action_gamesFragment_to_homeSettingsFragment"
|
||||||
app:destination="@id/homeSettingsFragment"
|
app:destination="@id/homeSettingsFragment"
|
||||||
@@ -16,6 +16,14 @@
|
|||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_gamesFragment_to_profileManagerFragment"
|
||||||
|
app:destination="@id/profileManagerFragment"
|
||||||
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
|
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/homeSettingsFragment"
|
android:id="@+id/homeSettingsFragment"
|
||||||
@@ -36,6 +44,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
||||||
app:destination="@id/gameFoldersFragment" />
|
app:destination="@id/gameFoldersFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_homeSettingsFragment_to_profileManagerFragment"
|
||||||
|
app:destination="@id/profileManagerFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
@@ -155,6 +166,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_perGamePropertiesFragment"
|
android:id="@+id/action_global_perGamePropertiesFragment"
|
||||||
app:destination="@id/perGamePropertiesFragment" />
|
app:destination="@id/perGamePropertiesFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_global_gameInfoFragment"
|
||||||
|
app:destination="@id/gameInfoFragment" />
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/gameInfoFragment"
|
android:id="@+id/gameInfoFragment"
|
||||||
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
|
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
|
||||||
@@ -186,5 +200,17 @@
|
|||||||
app:nullable="true"
|
app:nullable="true"
|
||||||
android:defaultValue="@null" />
|
android:defaultValue="@null" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/profileManagerFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.ProfileManagerFragment"
|
||||||
|
android:label="ProfileManagerFragment" >
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_profileManagerFragment_to_newUserDialog"
|
||||||
|
app:destination="@id/newUserDialogFragment" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/newUserDialogFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.EditUserDialogFragment"
|
||||||
|
android:label="NewUserDialogFragment" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|||||||
@@ -94,9 +94,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">مزامنة سرعة النواة</string>
|
<string name="use_sync_core">مزامنة سرعة النواة</string>
|
||||||
<string name="use_sync_core_description">قم بمزامنة سرعة النواة مع النسبة المئوية للسرعة القصوى لتحسين الأداء دون تغيير السرعة الفعلية للعبة.</string>
|
<string name="use_sync_core_description">قم بمزامنة سرعة النواة مع النسبة المئوية للسرعة القصوى لتحسين الأداء دون تغيير السرعة الفعلية للعبة.</string>
|
||||||
<string name="use_lru_cache">تمكين ذاكرة التخزين المؤقتة LRU</string>
|
|
||||||
<string name="use_lru_cache_description">قم بتمكين أو تعطيل ذاكرة التخزين المؤقتة الأقل استخدامًا (LRU)، مما يزيد من الأداء عن طريق توفير استخدام معالج وحدة المعالجة المركزية. قد تواجه بعض الألعاب مشكلات مع هذا الإعداد، لذا قم بتعطيله إذا لم يتم تشغيل اللعبة أو تعطلت بشكل عشوائي.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">تمكين محاكاة MMU المضيف</string>
|
<string name="cpuopt_unsafe_host_mmu">تمكين محاكاة MMU المضيف</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">يعمل هذا التحسين على تسريع وصول الذاكرة بواسطة البرنامج الضيف. يؤدي تمكينه إلى إجراء عمليات قراءة/كتابة ذاكرة الضيف مباشرة في الذاكرة والاستفادة من MMU المضيف. يؤدي تعطيل هذا إلى إجبار جميع عمليات الوصول إلى الذاكرة على استخدام محاكاة MMU البرمجية.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">يعمل هذا التحسين على تسريع وصول الذاكرة بواسطة البرنامج الضيف. يؤدي تمكينه إلى إجراء عمليات قراءة/كتابة ذاكرة الضيف مباشرة في الذاكرة والاستفادة من MMU المضيف. يؤدي تعطيل هذا إلى إجبار جميع عمليات الوصول إلى الذاكرة على استخدام محاكاة MMU البرمجية.</string>
|
||||||
<string name="debug_knobs">مقابض تصحيح الأخطاء</string>
|
<string name="debug_knobs">مقابض تصحيح الأخطاء</string>
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">مزامنة سرعة النواة</string>
|
<string name="use_sync_core">مزامنة سرعة النواة</string>
|
||||||
<string name="use_sync_core_description">خێرایی تیکەکانی ناوک ڕێکبخە لەگەڵ ڕێژەی خێرایی بەرزترین بۆ باشترکردنی کارایی بەبێ گۆڕینی خێرایی ڕاستەقینەی یارییەکە.</string>
|
<string name="use_sync_core_description">خێرایی تیکەکانی ناوک ڕێکبخە لەگەڵ ڕێژەی خێرایی بەرزترین بۆ باشترکردنی کارایی بەبێ گۆڕینی خێرایی ڕاستەقینەی یارییەکە.</string>
|
||||||
<string name="use_lru_cache">تمكين ذاكرة التخزين المؤقت LRU</string>
|
|
||||||
<string name="use_lru_cache_description">چالاک یان ناچالاککردنی کاشەی LRU، کارایی باشتر دەکات بە هەڵگرتنی بەکارهێنانی پرۆسەی CPU. هەندێک یاری کێشەی لەگەڵ هەیە، بەتایبەتی TotK 1.2.1، بۆیە بیخەوێنە ئەگەر یاریەکە نەگەڕێت یان بە هەڕەمەکی بشکێت.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">چالاککردنی میمیکردنی MMU میواندە</string>
|
<string name="cpuopt_unsafe_host_mmu">چالاککردنی میمیکردنی MMU میواندە</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">ئەم باشکردنە خێرایی دەستکەوتنی بیرگە لەلایەن پرۆگرامی میوانەکە زیاد دەکات. چالاککردنی وای لێدەکات کە خوێندنەوە/نووسینەکانی بیرگەی میوانەکە ڕاستەوخۆ لە بیرگە ئەنجام بدرێت و میمیکردنی MMU میواندە بەکاربهێنێت. ناچالاککردنی ئەمە هەموو دەستکەوتنەکانی بیرگە ڕەت دەکاتەوە لە بەکارهێنانی میمیکردنی MMU نەرمەکاڵا.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">ئەم باشکردنە خێرایی دەستکەوتنی بیرگە لەلایەن پرۆگرامی میوانەکە زیاد دەکات. چالاککردنی وای لێدەکات کە خوێندنەوە/نووسینەکانی بیرگەی میوانەکە ڕاستەوخۆ لە بیرگە ئەنجام بدرێت و میمیکردنی MMU میواندە بەکاربهێنێت. ناچالاککردنی ئەمە هەموو دەستکەوتنەکانی بیرگە ڕەت دەکاتەوە لە بەکارهێنانی میمیکردنی MMU نەرمەکاڵا.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -90,9 +90,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Synchronizovat rychlost jádra</string>
|
<string name="use_sync_core">Synchronizovat rychlost jádra</string>
|
||||||
<string name="use_sync_core_description">Synchronizuje rychlost jádra s nastaveným limitem rychlosti, aby se zlepšil výkon bez zrychlení samotné hry.</string>
|
<string name="use_sync_core_description">Synchronizuje rychlost jádra s nastaveným limitem rychlosti, aby se zlepšil výkon bez zrychlení samotné hry.</string>
|
||||||
<string name="use_lru_cache">Zapnout mezipaměť LRU</string>
|
|
||||||
<string name="use_lru_cache_description">Zapne, nebo vypne mezipaměť LRU (Least Recently Used). Ta zvyšuje výkon tím, že šetří využití procesoru. Některé hry s tímto mohou mít problém, takže pokud se hra nespustí, nebo náhodně padá, tuto volbu vypněte.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Povolit emulaci hostitelské MMU</string>
|
<string name="cpuopt_unsafe_host_mmu">Povolit emulaci hostitelské MMU</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Tato optimalizace zrychluje hostovanému programu přístupy do paměti. Zapnutím této funkce se čtení i zápisy do paměti provádějí přímo a využívají hostitelskou MMU. Při vypnutí musí všechny přístupy do paměti použít softwarovou emulaci MMU. </string>
|
<string name="cpuopt_unsafe_host_mmu_description">Tato optimalizace zrychluje hostovanému programu přístupy do paměti. Zapnutím této funkce se čtení i zápisy do paměti provádějí přímo a využívají hostitelskou MMU. Při vypnutí musí všechny přístupy do paměti použít softwarovou emulaci MMU. </string>
|
||||||
<string name="debug_knobs">Ladící parametry</string>
|
<string name="debug_knobs">Ladící parametry</string>
|
||||||
|
|||||||
@@ -90,9 +90,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Kern-Geschwindigkeit synchronisieren</string>
|
<string name="use_sync_core">Kern-Geschwindigkeit synchronisieren</string>
|
||||||
<string name="use_sync_core_description">Synchronisiert die Taktrate des Kerns mit der maximalen Geschwindigkeit, um die Leistung zu verbessern, ohne die tatsächliche Spielgeschwindigkeit zu verändern.</string>
|
<string name="use_sync_core_description">Synchronisiert die Taktrate des Kerns mit der maximalen Geschwindigkeit, um die Leistung zu verbessern, ohne die tatsächliche Spielgeschwindigkeit zu verändern.</string>
|
||||||
<string name="use_lru_cache">LRU-Cache aktivieren</string>
|
|
||||||
<string name="use_lru_cache_description">Aktivieren oder deaktivieren Sie den LRU-Cache, um die Leistung durch Einsparung von CPU-Prozessorauslastung zu verbessern. Einige Spiele haben Probleme damit, insbesondere TotK 1.2.1, deaktivieren Sie es also, wenn das Spiel nicht startet oder zufällig abstürzt.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Host-MMU-Emulation aktivieren</string>
|
<string name="cpuopt_unsafe_host_mmu">Host-MMU-Emulation aktivieren</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Diese Optimierung beschleunigt Speicherzugriffe durch das Gastprogramm. Wenn aktiviert, erfolgen Speicherlese- und -schreibvorgänge des Gastes direkt im Speicher und nutzen die MMU des Hosts. Das Deaktivieren erzwingt die Verwendung der Software-MMU-Emulation für alle Speicherzugriffe.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Diese Optimierung beschleunigt Speicherzugriffe durch das Gastprogramm. Wenn aktiviert, erfolgen Speicherlese- und -schreibvorgänge des Gastes direkt im Speicher und nutzen die MMU des Hosts. Das Deaktivieren erzwingt die Verwendung der Software-MMU-Emulation für alle Speicherzugriffe.</string>
|
||||||
<string name="debug_knobs">Debug-Regler</string>
|
<string name="debug_knobs">Debug-Regler</string>
|
||||||
@@ -232,6 +229,10 @@
|
|||||||
<string name="update_install_failed">Installieren des Updates fehlgeschlagen: %1$s</string>
|
<string name="update_install_failed">Installieren des Updates fehlgeschlagen: %1$s</string>
|
||||||
<string name="home_search">Suche</string>
|
<string name="home_search">Suche</string>
|
||||||
<string name="home_settings">Einstellungen</string>
|
<string name="home_settings">Einstellungen</string>
|
||||||
|
<string name="home_sort">Sortieren</string>
|
||||||
|
<string name="home_layout">Layout</string>
|
||||||
|
<string name="home_start">Start</string>
|
||||||
|
<string name="home_clear">Löschen</string>
|
||||||
<string name="empty_gamelist">Es wurden keine Dateien gefunden oder es wurde noch kein Spielverzeichnis ausgewählt.</string>
|
<string name="empty_gamelist">Es wurden keine Dateien gefunden oder es wurde noch kein Spielverzeichnis ausgewählt.</string>
|
||||||
<string name="manage_game_folders">Spiele-Ordner verwalten</string>
|
<string name="manage_game_folders">Spiele-Ordner verwalten</string>
|
||||||
<string name="select_games_folder_description">Erlaubt Eden die Spieleliste zu füllen</string>
|
<string name="select_games_folder_description">Erlaubt Eden die Spieleliste zu füllen</string>
|
||||||
@@ -708,6 +709,11 @@ Wird der Handheld-Modus verwendet, verringert es die Auflösung und erhöht die
|
|||||||
<string name="path_set">Pfad erfolgreich gesetzt</string>
|
<string name="path_set">Pfad erfolgreich gesetzt</string>
|
||||||
<string name="skip_migration">Überspringen</string>
|
<string name="skip_migration">Überspringen</string>
|
||||||
|
|
||||||
|
<!-- Footer actions -->
|
||||||
|
<string name="footer_open">Öffnen</string>
|
||||||
|
<string name="footer_game_info">Spielinfo</string>
|
||||||
|
<string name="footer_back">Zurück</string>
|
||||||
|
|
||||||
<!-- Game properties -->
|
<!-- Game properties -->
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_description">Programm-ID, Entwickler, Version</string>
|
<string name="info_description">Programm-ID, Entwickler, Version</string>
|
||||||
|
|||||||
@@ -75,9 +75,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Sincronizar la velocidad del núcleo</string>
|
<string name="use_sync_core">Sincronizar la velocidad del núcleo</string>
|
||||||
<string name="use_sync_core_description">Sincroniza la velocidad del núcleo con el porcentaje máximo de velocidad para mejorar el rendimiento sin alterar la velocidad real del juego.</string>
|
<string name="use_sync_core_description">Sincroniza la velocidad del núcleo con el porcentaje máximo de velocidad para mejorar el rendimiento sin alterar la velocidad real del juego.</string>
|
||||||
<string name="use_lru_cache">Habilitar caché LRU</string>
|
|
||||||
<string name="use_lru_cache_description">Habilite o deshabilite la caché menos utilizada recientemente (LRU), aumentando el rendimiento al ahorrar el uso del proceso de la CPU. Algunos juegos pueden ver problemas con esta configuración, así que desactívela si el juego no arranca o se bloquea aleatoriamente.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Habilitar emulación de MMU del anfitrión</string>
|
<string name="cpuopt_unsafe_host_mmu">Habilitar emulación de MMU del anfitrión</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Esta optimización acelera el acceso a la memoria del programa invitado. Al habilitarla, las lecturas y escrituras de la memoria del invitado se realizan directamente en la memoria y utilizan la MMU del anfitrión. Al deshabilitarla, todos los accesos a la memoria utilizan el software de emulación de la MMU.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Esta optimización acelera el acceso a la memoria del programa invitado. Al habilitarla, las lecturas y escrituras de la memoria del invitado se realizan directamente en la memoria y utilizan la MMU del anfitrión. Al deshabilitarla, todos los accesos a la memoria utilizan el software de emulación de la MMU.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -60,8 +60,6 @@
|
|||||||
<string name="sync_memory_operations_description">اطمینان از سازگاری دادهها بین عملیات محاسباتی و حافظه. این گزینه ممکن است مشکلات برخی بازیها را رفع کند، اما در برخی موارد ممکن است عملکرد را کاهش دهد. به نظر میرسد بازیهای با Unreal Engine 4 بیشترین تأثیر را داشته باشند.</string>
|
<string name="sync_memory_operations_description">اطمینان از سازگاری دادهها بین عملیات محاسباتی و حافظه. این گزینه ممکن است مشکلات برخی بازیها را رفع کند، اما در برخی موارد ممکن است عملکرد را کاهش دهد. به نظر میرسد بازیهای با Unreal Engine 4 بیشترین تأثیر را داشته باشند.</string>
|
||||||
<string name="buffer_reorder_disable">غیرفعال کردن مرتبسازی مجدد بافر</string>
|
<string name="buffer_reorder_disable">غیرفعال کردن مرتبسازی مجدد بافر</string>
|
||||||
<string name="buffer_reorder_disable_description">در صورت انتخاب، مرتبسازی مجدد آپلودهای حافظه نگاشتشده غیرفعال میشود که امکان ارتباط آپلودها با ترسیمات خاص را فراهم میکند. ممکن است در برخی موارد عملکرد را کاهش دهد.</string>
|
<string name="buffer_reorder_disable_description">در صورت انتخاب، مرتبسازی مجدد آپلودهای حافظه نگاشتشده غیرفعال میشود که امکان ارتباط آپلودها با ترسیمات خاص را فراهم میکند. ممکن است در برخی موارد عملکرد را کاهش دهد.</string>
|
||||||
<string name="use_lru_cache">فعالسازی حافظه نهان LRU</string>
|
|
||||||
<string name="use_lru_cache_description">حافظه پنهان LRU را فعال یا غیرفعال کنید تا با کاهش استفاده از پردازنده، عملکرد بهبود یابد. برخی بازیها مانند TotK 1.2.1 با این ویژگی مشکل دارند، در صورت عدم راهاندازی یا قطعی تصادفی بازی، آن را غیرفعال کنید.</string>
|
|
||||||
<string name="dyna_state">حالت پویای گسترده</string>
|
<string name="dyna_state">حالت پویای گسترده</string>
|
||||||
<string name="dyna_state_description">تعداد قابلیتهایی که میتوان در حالت Extended Dynamic State استفاده کرد را کنترل میکند. اعداد بالاتر قابلیتهای بیشتری را فعال کرده و میتوانند عملکرد را افزایش دهند، اما ممکن است با برخی درایورها و فروشندگان مشکلاتی ایجاد کنند. مقدار پیشفرض بسته به سیستم و قابلیتهای سختافزاری شما ممکن است متفاوت باشد. این مقدار را میتوان تغییر داد تا زمانی که پایداری و کیفیت بصری بهتری حاصل شود.</string>
|
<string name="dyna_state_description">تعداد قابلیتهایی که میتوان در حالت Extended Dynamic State استفاده کرد را کنترل میکند. اعداد بالاتر قابلیتهای بیشتری را فعال کرده و میتوانند عملکرد را افزایش دهند، اما ممکن است با برخی درایورها و فروشندگان مشکلاتی ایجاد کنند. مقدار پیشفرض بسته به سیستم و قابلیتهای سختافزاری شما ممکن است متفاوت باشد. این مقدار را میتوان تغییر داد تا زمانی که پایداری و کیفیت بصری بهتری حاصل شود.</string>
|
||||||
<string name="disabled">غیرفعال</string>
|
<string name="disabled">غیرفعال</string>
|
||||||
|
|||||||
@@ -75,9 +75,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Synchroniser la vitesse du cœur</string>
|
<string name="use_sync_core">Synchroniser la vitesse du cœur</string>
|
||||||
<string name="use_sync_core_description">Synchronise la vitesse du cœur avec le pourcentage de vitesse maximal pour améliorer les performances sans modifier la vitesse réelle du jeu.</string>
|
<string name="use_sync_core_description">Synchronise la vitesse du cœur avec le pourcentage de vitesse maximal pour améliorer les performances sans modifier la vitesse réelle du jeu.</string>
|
||||||
<string name="use_lru_cache">Activer le cache LRU</string>
|
|
||||||
<string name="use_lru_cache_description">Active ou désactive le cache LRU (Least Recently Used) pour améliorer les performances en réduisant l’utilisation du processeur. Certains jeux peuvent rencontrer des problèmes avec ce réglage ; désactivez-le si le jeu ne démarre pas ou plante de manière aléatoire.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Activer l\'émulation de la MMU hôte</string>
|
<string name="cpuopt_unsafe_host_mmu">Activer l\'émulation de la MMU hôte</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Cette optimisation accélère les accès mémoire par le programme invité. L\'activer entraîne que les lectures/écritures mémoire de l\'invité sont effectuées directement en mémoire et utilisent la MMU de l\'hôte. Désactiver cela force tous les accès mémoire à utiliser l\'émulation logicielle de la MMU.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Cette optimisation accélère les accès mémoire par le programme invité. L\'activer entraîne que les lectures/écritures mémoire de l\'invité sont effectuées directement en mémoire et utilisent la MMU de l\'hôte. Désactiver cela force tous les accès mémoire à utiliser l\'émulation logicielle de la MMU.</string>
|
||||||
<string name="debug_knobs">Boutons de débogage</string>
|
<string name="debug_knobs">Boutons de débogage</string>
|
||||||
|
|||||||
@@ -62,9 +62,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">סנכרון מהירות ליבה</string>
|
<string name="use_sync_core">סנכרון מהירות ליבה</string>
|
||||||
<string name="use_sync_core_description">סנכרן את מהירות הליבה לאחוז המהירות המרבי כדי לשפר ביצועים מבלי לשנות את מהירות המשחק בפועל.</string>
|
<string name="use_sync_core_description">סנכרן את מהירות הליבה לאחוז המהירות המרבי כדי לשפר ביצועים מבלי לשנות את מהירות המשחק בפועל.</string>
|
||||||
<string name="use_lru_cache">הפעלת מטמון LRU</string>
|
|
||||||
<string name="use_lru_cache_description">הפעל או השבת מטמון LRU לשיפור ביצועים על ידי חיסכון בשימוש במעבד. לחלק מהמשחקים כמו TotK 1.2.1 יש בעיות - השבת אם המשחק לא עולה או קורס באקראי.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">הפעל אמולציית MMU מארח</string>
|
<string name="cpuopt_unsafe_host_mmu">הפעל אמולציית MMU מארח</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">אופטימיזציה זו מאיצה את גישת הזיכרון על ידי התוכנית האורחת. הפעלתה גורמת לכך שפעולות קריאה/כתיבה לזיכרון האורח מתבצעות ישירות לזיכרון ומשתמשות ב-MMU של המארח. השבתת זאת מאלצת את כל גישות הזיכרון להשתמש באמולציית MMU תוכנתית.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">אופטימיזציה זו מאיצה את גישת הזיכרון על ידי התוכנית האורחת. הפעלתה גורמת לכך שפעולות קריאה/כתיבה לזיכרון האורח מתבצעות ישירות לזיכרון ומשתמשות ב-MMU של המארח. השבתת זאת מאלצת את כל גישות הזיכרון להשתמש באמולציית MMU תוכנתית.</string>
|
||||||
<string name="debug_knobs_description">לשימוש בפיתוח בלבד.</string>
|
<string name="debug_knobs_description">לשימוש בפיתוח בלבד.</string>
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Magsebesség szinkronizálása</string>
|
<string name="use_sync_core">Magsebesség szinkronizálása</string>
|
||||||
<string name="use_sync_core_description">A mag sebességének szinkronizálása a maximális sebesség százalékával a teljesítmény javítása érdekében a játék tényleges sebességének megváltoztatása nélkül.</string>
|
<string name="use_sync_core_description">A mag sebességének szinkronizálása a maximális sebesség százalékával a teljesítmény javítása érdekében a játék tényleges sebességének megváltoztatása nélkül.</string>
|
||||||
<string name="use_lru_cache">LRU gyorsítótár engedélyezése</string>
|
|
||||||
<string name="use_lru_cache_description">Engedélyezi vagy letiltja az LRU gyorsítótárat, növelve a teljesítményt a CPU használat csökkentésével. Néhány játéknak (különösen a TotK 1.2.1-nek) problémája lehet vele - tiltsa le, ha a játék nem indul el vagy véletlenszerűen összeomlik.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Gazda MMU emuláció engedélyezése</string>
|
<string name="cpuopt_unsafe_host_mmu">Gazda MMU emuláció engedélyezése</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Ez az optimalizáció gyorsítja a vendégprogram memória-hozzáférését. Engedélyezése esetén a vendég memóriaolvasási/írási műveletei közvetlenül a memóriában történnek, és kihasználják a gazda MMU-ját. Letiltás esetén minden memória-hozzáférés a szoftveres MMU emulációt használja.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Ez az optimalizáció gyorsítja a vendégprogram memória-hozzáférését. Engedélyezése esetén a vendég memóriaolvasási/írási műveletei közvetlenül a memóriában történnek, és kihasználják a gazda MMU-ját. Letiltás esetén minden memória-hozzáférés a szoftveres MMU emulációt használja.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -77,9 +77,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Sinkronisasi Kecepatan Inti</string>
|
<string name="use_sync_core">Sinkronisasi Kecepatan Inti</string>
|
||||||
<string name="use_sync_core_description">Sinkronkan kecepatan inti dengan persentase kecepatan maksimum untuk meningkatkan performa tanpa mengubah kecepatan sebenarnya dari permainan.</string>
|
<string name="use_sync_core_description">Sinkronkan kecepatan inti dengan persentase kecepatan maksimum untuk meningkatkan performa tanpa mengubah kecepatan sebenarnya dari permainan.</string>
|
||||||
<string name="use_lru_cache">Aktifkan LRU Cache</string>
|
|
||||||
<string name="use_lru_cache_description">Aktifkan atau nonaktifkan cache LRU untuk meningkatkan performa dengan mengurangi penggunaan proses CPU. Beberapa game seperti TotK 1.2.1 memiliki masalah - nonaktifkan jika game tidak mau berjalan atau crash acak.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Aktifkan Emulasi MMU Host</string>
|
<string name="cpuopt_unsafe_host_mmu">Aktifkan Emulasi MMU Host</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Optimasi ini mempercepat akses memori oleh program tamu. Mengaktifkannya menyebabkan pembacaan/penulisan memori tamu dilakukan langsung ke memori dan memanfaatkan MMU Host. Menonaktifkan ini memaksa semua akses memori menggunakan Emulasi MMU Perangkat Lunak.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Optimasi ini mempercepat akses memori oleh program tamu. Mengaktifkannya menyebabkan pembacaan/penulisan memori tamu dilakukan langsung ke memori dan memanfaatkan MMU Host. Menonaktifkan ini memaksa semua akses memori menggunakan Emulasi MMU Perangkat Lunak.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -75,9 +75,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Sincronizza velocità core</string>
|
<string name="use_sync_core">Sincronizza velocità core</string>
|
||||||
<string name="use_sync_core_description">Sincronizza la velocità del core con la percentuale massima di velocità per migliorare le prestazioni senza alterare la velocità effettiva del gioco.</string>
|
<string name="use_sync_core_description">Sincronizza la velocità del core con la percentuale massima di velocità per migliorare le prestazioni senza alterare la velocità effettiva del gioco.</string>
|
||||||
<string name="use_lru_cache">Abilita cache LRU</string>
|
|
||||||
<string name="use_lru_cache_description">Abilita o disabilita la cache LRU per migliorare le prestazioni riducendo l\'uso della CPU. Alcuni giochi come TotK 1.2.1 hanno problemi - disabilitalo se il gioco non si avvia o crasha casualmente.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Abilita l\'emulazione della MMU nell\'host</string>
|
<string name="cpuopt_unsafe_host_mmu">Abilita l\'emulazione della MMU nell\'host</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Questa ottimizzazione accelera gli accessi alla memoria da parte del programma guest. Abilitandola, le letture/scritture della memoria guest vengono eseguite direttamente in memoria e sfruttano la MMU host. Disabilitandola, tutti gli accessi alla memoria sono costretti a utilizzare l\'emulazione software della MMU.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Questa ottimizzazione accelera gli accessi alla memoria da parte del programma guest. Abilitandola, le letture/scritture della memoria guest vengono eseguite direttamente in memoria e sfruttano la MMU host. Disabilitandola, tutti gli accessi alla memoria sono costretti a utilizzare l\'emulazione software della MMU.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">コア速度の同期</string>
|
<string name="use_sync_core">コア速度の同期</string>
|
||||||
<string name="use_sync_core_description">コアの速度を最大速度パーセンテージに同期させ、ゲームの実際の速度を変えずにパフォーマンスを向上させます。</string>
|
<string name="use_sync_core_description">コアの速度を最大速度パーセンテージに同期させ、ゲームの実際の速度を変えずにパフォーマンスを向上させます。</string>
|
||||||
<string name="use_lru_cache">LRUキャッシュを有効化</string>
|
|
||||||
<string name="use_lru_cache_description">LRUキャッシュを有効/無効にし、CPUプロセスの使用を節約してパフォーマンスを向上させます。TotK 1.2.1など一部のゲームで問題が発生する可能性があるため、ゲームが起動しない場合やランダムにクラッシュする場合は無効にしてください。</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">ホストMMUエミュレーションを有効化</string>
|
<string name="cpuopt_unsafe_host_mmu">ホストMMUエミュレーションを有効化</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">この最適化により、ゲストプログラムによるメモリアクセスが高速化されます。有効にすると、ゲストのメモリ読み書きが直接メモリ内で実行され、ホストのMMUを利用します。無効にすると、すべてのメモリアクセスでソフトウェアMMUエミュレーションが使用されます。</string>
|
<string name="cpuopt_unsafe_host_mmu_description">この最適化により、ゲストプログラムによるメモリアクセスが高速化されます。有効にすると、ゲストのメモリ読み書きが直接メモリ内で実行され、ホストのMMUを利用します。無効にすると、すべてのメモリアクセスでソフトウェアMMUエミュレーションが使用されます。</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">코어 속도 동기화</string>
|
<string name="use_sync_core">코어 속도 동기화</string>
|
||||||
<string name="use_sync_core_description">코어 틱 속도를 최대 속도 백분율과 동기화하여 게임의 실제 속도를 변경하지 않고 성능을 향상시킵니다.</string>
|
<string name="use_sync_core_description">코어 틱 속도를 최대 속도 백분율과 동기화하여 게임의 실제 속도를 변경하지 않고 성능을 향상시킵니다.</string>
|
||||||
<string name="use_lru_cache">LRU 캐시 사용</string>
|
|
||||||
<string name="use_lru_cache_description">LRU 캐시를 활성화 또는 비활성화하여 CPU 프로세스 사용을 절약하고 성능을 향상시킵니다. TotK 1.2.1을 포함한 일부 게임에서 문제가 발생할 수 있으므로 게임이 부팅되지 않거나 무작위로 충돌하는 경우 비활성화하세요.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">호스트 MMU 에뮬레이션 사용</string>
|
<string name="cpuopt_unsafe_host_mmu">호스트 MMU 에뮬레이션 사용</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">이 최적화는 게스트 프로그램의 메모리 접근 속도를 높입니다. 활성화하면 게스트의 메모리 읽기/쓰기가 메모리에서 직접 수행되고 호스트의 MMU를 활용합니다. 비활성화하면 모든 메모리 접근에 소프트웨어 MMU 에뮬레이션을 사용하게 됩니다.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">이 최적화는 게스트 프로그램의 메모리 접근 속도를 높입니다. 활성화하면 게스트의 메모리 읽기/쓰기가 메모리에서 직접 수행되고 호스트의 MMU를 활용합니다. 비활성화하면 모든 메모리 접근에 소프트웨어 MMU 에뮬레이션을 사용하게 됩니다.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Synkroniser kjernespeed</string>
|
<string name="use_sync_core">Synkroniser kjernespeed</string>
|
||||||
<string name="use_sync_core_description">Synkroniser kjernens hastighet med maksimal hastighetsprosent for å forbedre ytelsen uten å endre spillets faktiske hastighet.</string>
|
<string name="use_sync_core_description">Synkroniser kjernens hastighet med maksimal hastighetsprosent for å forbedre ytelsen uten å endre spillets faktiske hastighet.</string>
|
||||||
<string name="use_lru_cache">Aktiver LRU-mellomlager</string>
|
|
||||||
<string name="use_lru_cache_description">Aktiver eller deaktiver LRU-mellomlager for å forbedre ytelsen ved å spare CPU-prosessorbruk. Noen spill som TotK 1.2.1 har problemer med dette - deaktiver hvis spillet ikke starter eller krasjer tilfeldig.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Aktiver verts-MMU-emulering</string>
|
<string name="cpuopt_unsafe_host_mmu">Aktiver verts-MMU-emulering</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Denne optimaliseringen fremskynder minnetilgang av gjesteprogrammet. Hvis aktivert, utføres gjestens minnelesing/skriving direkte i minnet og bruker vertens MMU. Deaktivering tvinger alle minnetilganger til å bruke programvarebasert MMU-emulering.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Denne optimaliseringen fremskynder minnetilgang av gjesteprogrammet. Hvis aktivert, utføres gjestens minnelesing/skriving direkte i minnet og bruker vertens MMU. Deaktivering tvinger alle minnetilganger til å bruke programvarebasert MMU-emulering.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -90,9 +90,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Synchronizuj prędkość rdzenia</string>
|
<string name="use_sync_core">Synchronizuj prędkość rdzenia</string>
|
||||||
<string name="use_sync_core_description">Synchronizuje prędkość rdzenia z maksymalnym procentem prędkości, aby poprawić wydajność bez zmiany rzeczywistej prędkości gry.</string>
|
<string name="use_sync_core_description">Synchronizuje prędkość rdzenia z maksymalnym procentem prędkości, aby poprawić wydajność bez zmiany rzeczywistej prędkości gry.</string>
|
||||||
<string name="use_lru_cache">Włącz pamięć podręczną LRU</string>
|
|
||||||
<string name="use_lru_cache_description">Włącz lub wyłącz pamięć podręczną LRU, aby poprawić wydajność poprzez zmniejszenie użycia procesora. Niektóre gry, takie jak TotK 1.2.1, mogą mieć problemy - wyłącz, jeśli gra się nie uruchamia lub losowo zawiesza.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Włącz emulację MMU hosta</string>
|
<string name="cpuopt_unsafe_host_mmu">Włącz emulację MMU hosta</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Ta optymalizacja przyspiesza dostęp do pamięci przez program gościa. Włączenie powoduje, że odczyty/zapisy pamięci gościa są wykonywane bezpośrednio w pamięci i wykorzystują MMU hosta. Wyłączenie wymusza użycie programowej emulacji MMU dla wszystkich dostępów do pamięci.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Ta optymalizacja przyspiesza dostęp do pamięci przez program gościa. Włączenie powoduje, że odczyty/zapisy pamięci gościa są wykonywane bezpośrednio w pamięci i wykorzystują MMU hosta. Wyłączenie wymusza użycie programowej emulacji MMU dla wszystkich dostępów do pamięci.</string>
|
||||||
<string name="debug_knobs">Przełączniki debugowania</string>
|
<string name="debug_knobs">Przełączniki debugowania</string>
|
||||||
|
|||||||
@@ -84,9 +84,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Sincronizar Velocidade do Núcleo</string>
|
<string name="use_sync_core">Sincronizar Velocidade do Núcleo</string>
|
||||||
<string name="use_sync_core_description">Aumenta a velocidade do ciclo do núcleo até o máximo para melhorar o desempenho, mantendo a velocidade real do jogo inalterada.</string>
|
<string name="use_sync_core_description">Aumenta a velocidade do ciclo do núcleo até o máximo para melhorar o desempenho, mantendo a velocidade real do jogo inalterada.</string>
|
||||||
<string name="use_lru_cache">Ativar Cache LRU</string>
|
|
||||||
<string name="use_lru_cache_description">O cache Least Recently Used (LRU, ou Itens Menos Usados) aumenta o desempenho, economizando o uso do processador. Alguns jogos podem apresentar problemas com esta configuração. Desative-a caso o jogo não inicie ou trave aleatoriamente.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Ativar Emulação da MMU do Sistema</string>
|
<string name="cpuopt_unsafe_host_mmu">Ativar Emulação da MMU do Sistema</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Esta otimização acelera o acesso à memória pelo programa. Ao ativá-la, leituras e gravações são feitas diretamente na memória, usando a MMU (Unidade de Gerenciamento de Memória) do sistema. Ao desativar, todos os acessos passam a utilizar a Emulação de MMU por Software.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Esta otimização acelera o acesso à memória pelo programa. Ao ativá-la, leituras e gravações são feitas diretamente na memória, usando a MMU (Unidade de Gerenciamento de Memória) do sistema. Ao desativar, todos os acessos passam a utilizar a Emulação de MMU por Software.</string>
|
||||||
<string name="debug_knobs">Controles de Depuração</string>
|
<string name="debug_knobs">Controles de Depuração</string>
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Sincronizar velocidade do núcleo</string>
|
<string name="use_sync_core">Sincronizar velocidade do núcleo</string>
|
||||||
<string name="use_sync_core_description">Sincroniza a velocidade do núcleo com a percentagem máxima de velocidade para melhorar o desempenho sem alterar a velocidade real do jogo.</string>
|
<string name="use_sync_core_description">Sincroniza a velocidade do núcleo com a percentagem máxima de velocidade para melhorar o desempenho sem alterar a velocidade real do jogo.</string>
|
||||||
<string name="use_lru_cache">Ativar cache LRU</string>
|
|
||||||
<string name="use_lru_cache_description">Ative ou desative a cache LRU para melhorar desempenho poupando uso do processador. Alguns jogos como TotK 1.2.1 têm problemas - desative se o jogo não iniciar ou falhar aleatoriamente.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Ativar Emulação de MMU do Anfitrião</string>
|
<string name="cpuopt_unsafe_host_mmu">Ativar Emulação de MMU do Anfitrião</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Esta otimização acelera os acessos à memória pelo programa convidado. Ativar faz com que as leituras/escritas de memória do convidado sejam efetuadas diretamente na memória e utilizem a MMU do Anfitrião. Desativar força todos os acessos à memória a usar a Emulação de MMU por Software.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Esta otimização acelera os acessos à memória pelo programa convidado. Ativar faz com que as leituras/escritas de memória do convidado sejam efetuadas diretamente na memória e utilizem a MMU do Anfitrião. Desativar força todos os acessos à memória a usar a Emulação de MMU por Software.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -92,9 +92,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Синхронизация скорости ядра</string>
|
<string name="use_sync_core">Синхронизация скорости ядра</string>
|
||||||
<string name="use_sync_core_description">Синхронизирует скорость ядра с максимальным процентом скорости для улучшения производительности без изменения фактической скорости игры.</string>
|
<string name="use_sync_core_description">Синхронизирует скорость ядра с максимальным процентом скорости для улучшения производительности без изменения фактической скорости игры.</string>
|
||||||
<string name="use_lru_cache">Включить LRU-кеш</string>
|
|
||||||
<string name="use_lru_cache_description">Включите или отключите кэш LRU (наименее недавно использованный), повышая производительность за счёт снижения нагрузки на ЦП. Некоторые игры могут работать с ним некорректно (например, TotK 1.2.1), поэтому отключите, если игра не запускается или случайно вылетает.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Включить эмуляцию MMU хоста</string>
|
<string name="cpuopt_unsafe_host_mmu">Включить эмуляцию MMU хоста</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Эта оптимизация ускоряет доступ к памяти гостевой программой. При включении операции чтения/записи памяти гостя выполняются напрямую в памяти с использованием MMU хоста. Отключение заставляет все обращения к памяти использовать программную эмуляцию MMU.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Эта оптимизация ускоряет доступ к памяти гостевой программой. При включении операции чтения/записи памяти гостя выполняются напрямую в памяти с использованием MMU хоста. Отключение заставляет все обращения к памяти использовать программную эмуляцию MMU.</string>
|
||||||
<string name="debug_knobs">Настройки отладки</string>
|
<string name="debug_knobs">Настройки отладки</string>
|
||||||
|
|||||||
@@ -56,9 +56,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Синхронизујте брзину језгре</string>
|
<string name="use_sync_core">Синхронизујте брзину језгре</string>
|
||||||
<string name="use_sync_core_description">Синхронизујте језгро Тицк Брзина брзине максималне брзине да бисте побољшали перформансе без измене стварне брзине игре.</string>
|
<string name="use_sync_core_description">Синхронизујте језгро Тицк Брзина брзине максималне брзине да бисте побољшали перформансе без измене стварне брзине игре.</string>
|
||||||
<string name="use_lru_cache">Омогући ЛРУ кеш</string>
|
|
||||||
<string name="use_lru_cache_description">Омогућите или онемогућите најмање недавно коришћене (ЛРУ) кеш меморије, све веће перформансе штедећи употребу процеса ЦПУ-а. Неке игре са њом имају проблеме, посебно тотк 1.2.1, тако да онемогућите ако се игра не покрене или се руши насумично.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Омогући емулацију MMU домаћина</string>
|
<string name="cpuopt_unsafe_host_mmu">Омогући емулацију MMU домаћина</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Ова оптимизација убрзава приступ меморији од стране гостујућег програма. Укључивање изазива да се читања/уписа меморије госта обављају директно у меморији и користе MMU домаћина. Искључивање присиљава све приступе меморији да користе софтверску емулацију MMU.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Ова оптимизација убрзава приступ меморији од стране гостујућег програма. Укључивање изазива да се читања/уписа меморије госта обављају директно у меморији и користе MMU домаћина. Искључивање присиљава све приступе меморији да користе софтверску емулацију MMU.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -94,9 +94,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Синхронізувати швидкість ядра</string>
|
<string name="use_sync_core">Синхронізувати швидкість ядра</string>
|
||||||
<string name="use_sync_core_description">Синхронізує швидкість ядра з максимальним відсотком швидкості для покращення продуктивності без зміни реальної швидкості гри.</string>
|
<string name="use_sync_core_description">Синхронізує швидкість ядра з максимальним відсотком швидкості для покращення продуктивності без зміни реальної швидкості гри.</string>
|
||||||
<string name="use_lru_cache">Увімкнути LRU-кеш</string>
|
|
||||||
<string name="use_lru_cache_description">Увімкніть або вимкніть кеш LRU (Least Recently Used) для покращення продуктивності шляхом зменшення навантаження на CPU. Деякі ігри (зокрема TotK 1.2.1) можуть працювати некоректно - вимкніть, якщо гра не запускається або раптово вилітає.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Увімкнути емуляцію MMU хоста</string>
|
<string name="cpuopt_unsafe_host_mmu">Увімкнути емуляцію MMU хоста</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Ця оптимізація пришвидшує доступ до пам\'яті гостьовою програмою. Увімкнення призводить до того, що читання/запис пам\'яті гостя виконуються безпосередньо в пам\'яті та використовують MMU хоста. Вимкнення змушує всі звернення до пам\'яті використовувати програмну емуляцію MMU.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Ця оптимізація пришвидшує доступ до пам\'яті гостьовою програмою. Увімкнення призводить до того, що читання/запис пам\'яті гостя виконуються безпосередньо в пам\'яті та використовують MMU хоста. Вимкнення змушує всі звернення до пам\'яті використовувати програмну емуляцію MMU.</string>
|
||||||
<string name="debug_knobs">Зневаджувальні регулятори</string>
|
<string name="debug_knobs">Зневаджувальні регулятори</string>
|
||||||
|
|||||||
@@ -58,9 +58,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Đồng bộ tốc độ lõi</string>
|
<string name="use_sync_core">Đồng bộ tốc độ lõi</string>
|
||||||
<string name="use_sync_core_description">Đồng bộ tốc độ lõi với tỷ lệ phần trăm tốc độ tối đa để cải thiện hiệu suất mà không làm thay đổi tốc độ thực tế của trò chơi.</string>
|
<string name="use_sync_core_description">Đồng bộ tốc độ lõi với tỷ lệ phần trăm tốc độ tối đa để cải thiện hiệu suất mà không làm thay đổi tốc độ thực tế của trò chơi.</string>
|
||||||
<string name="use_lru_cache">Bật bộ nhớ đệm LRU</string>
|
|
||||||
<string name="use_lru_cache_description">Bật hoặc tắt bộ nhớ đệm LRU để cải thiện hiệu suất bằng cách tiết kiệm quy trình sử dụng CPU. Một số trò chơi như TotK 1.2.1 có vấn đề - hãy tắt nếu trò chơi không khởi động hoặc bị treo ngẫu nhiên.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Bật giả lập MMU Máy chủ</string>
|
<string name="cpuopt_unsafe_host_mmu">Bật giả lập MMU Máy chủ</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">Tối ưu hóa này tăng tốc độ truy cập bộ nhớ của chương trình khách. Bật nó lên khiến các thao tác đọc/ghi bộ nhớ khách được thực hiện trực tiếp vào bộ nhớ và sử dụng MMU của Máy chủ. Tắt tính năng này buộc tất cả quyền truy cập bộ nhớ phải sử dụng Giả lập MMU Phần mềm.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">Tối ưu hóa này tăng tốc độ truy cập bộ nhớ của chương trình khách. Bật nó lên khiến các thao tác đọc/ghi bộ nhớ khách được thực hiện trực tiếp vào bộ nhớ và sử dụng MMU của Máy chủ. Tắt tính năng này buộc tất cả quyền truy cập bộ nhớ phải sử dụng Giả lập MMU Phần mềm.</string>
|
||||||
<!-- NVDEC Emulation -->
|
<!-- NVDEC Emulation -->
|
||||||
|
|||||||
@@ -94,9 +94,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">同步核心速度</string>
|
<string name="use_sync_core">同步核心速度</string>
|
||||||
<string name="use_sync_core_description">将核心速度与最大速度百分比同步,在不改变游戏实际速度的情况下提高性能。</string>
|
<string name="use_sync_core_description">将核心速度与最大速度百分比同步,在不改变游戏实际速度的情况下提高性能。</string>
|
||||||
<string name="use_lru_cache">启用LRU缓存</string>
|
|
||||||
<string name="use_lru_cache_description">启用或禁用LRU缓存,通过节省CPU进程使用来提高性能。某些游戏可能存在问题,特别是TotK 1.2.1,如果游戏无法启动或随机崩溃,请禁用此选项。</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">启用主机 MMU 模拟</string>
|
<string name="cpuopt_unsafe_host_mmu">启用主机 MMU 模拟</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">此优化可加速来宾程序的内存访问。启用后,来宾内存读取/写入将直接在内存中执行并利用主机的 MMU。禁用此功能将强制所有内存访问使用软件 MMU 模拟。</string>
|
<string name="cpuopt_unsafe_host_mmu_description">此优化可加速来宾程序的内存访问。启用后,来宾内存读取/写入将直接在内存中执行并利用主机的 MMU。禁用此功能将强制所有内存访问使用软件 MMU 模拟。</string>
|
||||||
<string name="debug_knobs">调试开关</string>
|
<string name="debug_knobs">调试开关</string>
|
||||||
|
|||||||
@@ -90,9 +90,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">同步核心速度</string>
|
<string name="use_sync_core">同步核心速度</string>
|
||||||
<string name="use_sync_core_description">將核心速度與最大速度百分比同步,在不改變遊戲實際速度的情況下提高效能。</string>
|
<string name="use_sync_core_description">將核心速度與最大速度百分比同步,在不改變遊戲實際速度的情況下提高效能。</string>
|
||||||
<string name="use_lru_cache">啟用LRU快取</string>
|
|
||||||
<string name="use_lru_cache_description">啟用或停用LRU快取,透過節省CPU進程使用率來提高效能。某些遊戲可能存在問題,特別是薩爾達傳說:王國之淚 v1.2.1(更新),如果遊戲無法啟動或隨機崩潰,請停用此選項。</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">啟用主機 MMU 模擬</string>
|
<string name="cpuopt_unsafe_host_mmu">啟用主機 MMU 模擬</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">此最佳化可加速來賓程式的記憶體存取。啟用後,來賓記憶體讀取/寫入將直接在記憶體中執行並利用主機的 MMU。停用此功能將強制所有記憶體存取使用軟體 MMU 模擬。</string>
|
<string name="cpuopt_unsafe_host_mmu_description">此最佳化可加速來賓程式的記憶體存取。啟用後,來賓記憶體讀取/寫入將直接在記憶體中執行並利用主機的 MMU。停用此功能將強制所有記憶體存取使用軟體 MMU 模擬。</string>
|
||||||
<string name="debug_knobs">偵錯開關</string>
|
<string name="debug_knobs">偵錯開關</string>
|
||||||
|
|||||||
@@ -99,9 +99,6 @@
|
|||||||
|
|
||||||
<string name="use_sync_core">Synchronize Core Speed</string>
|
<string name="use_sync_core">Synchronize Core Speed</string>
|
||||||
<string name="use_sync_core_description">Synchronize the core tick speed to the maximum speed percentage to improve performance without altering the game\'s actual speed.</string>
|
<string name="use_sync_core_description">Synchronize the core tick speed to the maximum speed percentage to improve performance without altering the game\'s actual speed.</string>
|
||||||
<string name="use_lru_cache">Enable LRU Cache</string>
|
|
||||||
<string name="use_lru_cache_description">Enable or disable the Least Recently Used (LRU) cache, increasing performance by saving CPU process usage. Some games may see issues with this setting, so disable it if the game doesn\'t boot or crashes randomly.</string>
|
|
||||||
|
|
||||||
<string name="cpuopt_unsafe_host_mmu">Enable Host MMU Emulation</string>
|
<string name="cpuopt_unsafe_host_mmu">Enable Host MMU Emulation</string>
|
||||||
<string name="cpuopt_unsafe_host_mmu_description">This optimization speeds up memory accesses by the guest program. Enabling it causes guest memory reads/writes to be done directly into memory and make use of Host\'s MMU. Disabling this forces all memory accesses to use Software MMU Emulation.</string>
|
<string name="cpuopt_unsafe_host_mmu_description">This optimization speeds up memory accesses by the guest program. Enabling it causes guest memory reads/writes to be done directly into memory and make use of Host\'s MMU. Disabling this forces all memory accesses to use Software MMU Emulation.</string>
|
||||||
<string name="debug_knobs">Debug knobs</string>
|
<string name="debug_knobs">Debug knobs</string>
|
||||||
@@ -243,6 +240,10 @@
|
|||||||
<string name="update_install_failed">Failed to install update: %1$s</string>
|
<string name="update_install_failed">Failed to install update: %1$s</string>
|
||||||
<string name="home_search">Search</string>
|
<string name="home_search">Search</string>
|
||||||
<string name="home_settings">Settings</string>
|
<string name="home_settings">Settings</string>
|
||||||
|
<string name="home_sort">Sort</string>
|
||||||
|
<string name="home_layout">Layout</string>
|
||||||
|
<string name="home_start">Start</string>
|
||||||
|
<string name="home_clear">Clear</string>
|
||||||
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
||||||
<string name="manage_game_folders">Manage game folders</string>
|
<string name="manage_game_folders">Manage game folders</string>
|
||||||
<string name="select_games_folder_description">Allows Eden to populate the games list</string>
|
<string name="select_games_folder_description">Allows Eden to populate the games list</string>
|
||||||
@@ -381,9 +382,9 @@
|
|||||||
<string name="qlaunch_description">Launch applications from the system home screen</string>
|
<string name="qlaunch_description">Launch applications from the system home screen</string>
|
||||||
<string name="applets">Applet launcher</string>
|
<string name="applets">Applet launcher</string>
|
||||||
<string name="applets_description">Launch system applets using installed firmware</string>
|
<string name="applets_description">Launch system applets using installed firmware</string>
|
||||||
<string name="applets_error_firmware">Firmware not installed or invalid version</string>
|
<string name="applets_error_firmware">Firmware not installed</string>
|
||||||
<string name="applets_error_applet">Applet not available</string>
|
<string name="applets_error_applet">Applet not available</string>
|
||||||
<string name="applets_error_description"><![CDATA[Please ensure your <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> file and <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-system-firmware\">firmware</a> are installed and try again.<br>Additionally, ensure your firmware is of version 19.0.1 or older.]]></string>
|
<string name="applets_error_description"><![CDATA[Please ensure your <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> file and <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-system-firmware\">firmware</a> are installed and try again.]]></string>
|
||||||
<string name="album_applet">Album</string>
|
<string name="album_applet">Album</string>
|
||||||
<string name="album_applet_description">See images stored in the user screenshots folder with the system photo viewer</string>
|
<string name="album_applet_description">See images stored in the user screenshots folder with the system photo viewer</string>
|
||||||
<string name="mii_edit_applet">Mii edit</string>
|
<string name="mii_edit_applet">Mii edit</string>
|
||||||
@@ -769,6 +770,11 @@
|
|||||||
<string name="path_set">Path set successfully</string>
|
<string name="path_set">Path set successfully</string>
|
||||||
<string name="skip_migration">Skip</string>
|
<string name="skip_migration">Skip</string>
|
||||||
|
|
||||||
|
<!-- Footer actions -->
|
||||||
|
<string name="footer_open">Open</string>
|
||||||
|
<string name="footer_game_info">Game Info</string>
|
||||||
|
<string name="footer_back">Back</string>
|
||||||
|
|
||||||
<!-- Game properties -->
|
<!-- Game properties -->
|
||||||
<string name="info">Info</string>
|
<string name="info">Info</string>
|
||||||
<string name="info_description">Program ID, developer, version</string>
|
<string name="info_description">Program ID, developer, version</string>
|
||||||
@@ -1239,6 +1245,35 @@
|
|||||||
<string name="enable_overlay">Enable Overlay Applet</string>
|
<string name="enable_overlay">Enable Overlay Applet</string>
|
||||||
<string name="enable_overlay_description">Enables Horizon\'s built-in overlay applet. Press and hold the home button for 1 second to show it.</string>
|
<string name="enable_overlay_description">Enables Horizon\'s built-in overlay applet. Press and hold the home button for 1 second to show it.</string>
|
||||||
|
|
||||||
|
<!-- Profile Management -->
|
||||||
|
<string name="profile_manager">Profile Manager</string>
|
||||||
|
<string name="profile_manager_description">Manage user profiles</string>
|
||||||
|
<string name="profile_add_user">Add User</string>
|
||||||
|
<string name="profile_new_user">New User</string>
|
||||||
|
<string name="profile_edit_user">Edit User</string>
|
||||||
|
<string name="profile_edit">Edit</string>
|
||||||
|
<string name="profile_delete">Delete</string>
|
||||||
|
<string name="profile_username">Username</string>
|
||||||
|
<string name="profile_uuid">User ID (UUID)</string>
|
||||||
|
<string name="profile_uuid_description">This is the unique identifier for this user profile. It cannot be changed after creation.</string>
|
||||||
|
<string name="profile_generate">Generate</string>
|
||||||
|
<string name="profile_avatar">User Avatar</string>
|
||||||
|
<string name="profile_select_image">Select Image</string>
|
||||||
|
<string name="profile_firmware_avatars">Firmware Avatars</string>
|
||||||
|
<string name="profile_firmware_avatars_unavailable">Firmware avatars are not available. Please install firmware to use this feature.</string>
|
||||||
|
<string name="profile_revert_image">Revert to Default</string>
|
||||||
|
<string name="profile_current_user">Current User</string>
|
||||||
|
<string name="profile_max_users_title">Maximum Users Reached</string>
|
||||||
|
<string name="profile_max_users_message">You cannot create more than 8 user profiles. Please delete an existing profile to create a new one.</string>
|
||||||
|
<string name="profile_delete_confirm_title">Delete Profile?</string>
|
||||||
|
<string name="profile_delete_confirm_message">Are you sure you want to delete %1$s? All save data for this user will be deleted.</string>
|
||||||
|
<string name="profile_delete_current_user_message">Are you sure you want to delete %1$s? This is the currently selected user. The first available user will be selected instead.</string>
|
||||||
|
<string name="profile_create_failed">Failed to create user profile</string>
|
||||||
|
<string name="profile_update_failed">Failed to update user profile</string>
|
||||||
|
<string name="profile_image_load_error">Failed to load image: %1$s</string>
|
||||||
|
<string name="profile_image_save_error">Failed to save image: %1$s</string>
|
||||||
|
<string name="error">Error</string>
|
||||||
|
|
||||||
<!-- Licenses screen strings -->
|
<!-- Licenses screen strings -->
|
||||||
<string name="licenses">Licenses</string>
|
<string name="licenses">Licenses</string>
|
||||||
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
|
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
|
||||||
@@ -1745,4 +1780,8 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="external_content">External Content</string>
|
||||||
|
<string name="add_folders">Add Folder</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user