Compare commits

..

6 Commits

Author SHA1 Message Date
lizzie a60c5ddde2 slug you 2026-01-30 21:07:02 +01:00
lizzie ab24a002f2 actually derivates are fine? 2026-01-30 21:07:02 +01:00
lizzie 817582463b handle null du/dx, dv/dy 2026-01-30 21:07:02 +01:00
lizzie 2ad9160d59 make silksongy worky againy 2026-01-30 21:07:02 +01:00
lizzie 1f35996959 license 2026-01-30 21:07:02 +01:00
lizzie d3ed681bc1 [maxwell/fermi_2d] gutter entire Software blitter
Signed-off-by: lizzie <lizzie@eden-emu.dev>
2026-01-30 21:07:02 +01:00
381 changed files with 4442 additions and 14258 deletions
+2 -18
View File
@@ -1,6 +1,6 @@
#!/bin/sh -e
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
NUM_JOBS=$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)
@@ -29,7 +29,6 @@ Options:
-b, --build-type <TYPE> Build type (variable: TYPE)
Valid values are: Release, RelWithDebInfo, Debug
Default: Debug
-n, --nightly Create a nightly build.
Extra arguments are passed to CMake (e.g. -DCMAKE_OPTION_NAME=VALUE)
Set the CCACHE variable to "true" to enable build caching.
@@ -62,7 +61,6 @@ while true; do
-r|--release) DEVEL=false ;;
-t|--target) target "$2"; shift ;;
-b|--build-type) type "$2"; shift ;;
-n|--nightly) NIGHTLY=true ;;
-h|--help) usage ;;
*) break ;;
esac
@@ -103,20 +101,7 @@ cd src/android
chmod +x ./gradlew
set -- "$@" -DUSE_CCACHE="${CCACHE}"
nightly() {
[ "$NIGHTLY" = "true" ]
}
if nightly || [ "$DEVEL" != "true" ]; then
set -- "$@" -DENABLE_UPDATE_CHECKER=ON
fi
if nightly; then
NIGHTLY=true
else
NIGHTLY=false
fi
[ "$DEVEL" != "true" ] && set -- "$@" -DENABLE_UPDATE_CHECKER=ON
echo "-- building..."
@@ -125,7 +110,6 @@ echo "-- building..."
-Dorg.gradle.parallel="${CCACHE}" \
-Dorg.gradle.workers.max="${NUM_JOBS}" \
-PYUZU_ANDROID_ARGS="$*" \
-Pnightly="$NIGHTLY" \
--info
if [ -n "${ANDROID_KEYSTORE_B64}" ]; then
+3 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash -e
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
case "$1" in
@@ -104,7 +104,8 @@ cmake .. -G Ninja \
-DYUZU_USE_QT_MULTIMEDIA=$MULTIMEDIA \
-DYUZU_USE_QT_WEB_ENGINE=$WEBENGINE \
-DYUZU_USE_FASTER_LD=ON \
-DENABLE_LTO=ON \
-DYUZU_ENABLE_LTO=ON \
-DDYNARMIC_ENABLE_LTO=ON \
"${EXTRA_CMAKE_FLAGS[@]}"
ninja -j${NPROC}
Executable → Regular
+3 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash -ex
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
if [ "$COMPILER" == "clang" ]
@@ -32,8 +32,9 @@ cmake .. -G Ninja \
-DYUZU_ROOM_STANDALONE=OFF \
-DYUZU_USE_QT_MULTIMEDIA=${USE_MULTIMEDIA:-false} \
-DYUZU_USE_QT_WEB_ENGINE=${USE_WEBENGINE:-false} \
-DENABLE_LTO=ON \
-DYUZU_ENABLE_LTO=ON \
-DCMAKE_EXE_LINKER_FLAGS=" /LTCG" \
-DDYNARMIC_ENABLE_LTO=ON \
-DYUZU_USE_BUNDLED_QT=${BUNDLE_QT:-false} \
-DUSE_CCACHE=${CCACHE:-false} \
-DENABLE_UPDATE_CHECKER=${DEVEL:-true} \
+11
View File
@@ -0,0 +1,11 @@
--- a/libs/context/CMakeLists.txt 2025-09-08 00:42:31.303651800 -0400
+++ b/libs/context/CMakeLists.txt 2025-09-08 00:42:40.592184300 -0400
@@ -146,7 +146,7 @@
set(ASM_LANGUAGE ASM)
endif()
elseif(BOOST_CONTEXT_ASSEMBLER STREQUAL armasm)
- set(ASM_LANGUAGE ASM_ARMASM)
+ set(ASM_LANGUAGE ASM_MARMASM)
else()
set(ASM_LANGUAGE ASM_MASM)
endif()
+14
View File
@@ -0,0 +1,14 @@
diff --git a/libs/context/CMakeLists.txt b/libs/context/CMakeLists.txt
index 8210f65..0e59dd7 100644
--- a/libs/context/CMakeLists.txt
+++ b/libs/context/CMakeLists.txt
@@ -186,7 +186,8 @@ if(BOOST_CONTEXT_IMPLEMENTATION STREQUAL "fcontext")
set_property(SOURCE ${ASM_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "/safeseh")
endif()
- else() # masm
+ # armasm doesn't support most of these options
+ elseif(NOT BOOST_CONTEXT_ASSEMBLER STREQUAL armasm) # masm
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set_property(SOURCE ${ASM_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "-x" "assembler-with-cpp")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+14 -24
View File
@@ -1,62 +1,52 @@
From 436fc1978c78edd085d99b33275b24be0ac96aa0 Mon Sep 17 00:00:00 2001
From e1a946ffb79022d38351a0623f819a5419965c3e Mon Sep 17 00:00:00 2001
From: crueter <crueter@eden-emu.dev>
Date: Sun, 1 Feb 2026 16:21:10 -0500
Subject: [PATCH] Fix build on MinGW
Date: Fri, 24 Oct 2025 23:41:09 -0700
Subject: [PATCH] [build] Fix MinGW missing GetAddrInfoExCancel definition
MinGW doesn't define GetAddrInfoExCancel.
MinGW does not define GetAddrInfoExCancel in its wstcpi whatever header,
so to get around this we can just load it with GetProcAddress et al.
Signed-off-by: crueter <crueter@eden-emu.dev>
---
httplib.h | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
httplib.h | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/httplib.h b/httplib.h
index ec8d2a2..5f9a510 100644
index e15ba44..90a76dc 100644
--- a/httplib.h
+++ b/httplib.h
@@ -203,14 +203,17 @@
@@ -203,11 +203,13 @@
#error Sorry, Visual Studio versions prior to 2015 are not supported
#endif
-#pragma comment(lib, "ws2_32.lib")
-
#ifndef _SSIZE_T_DEFINED
using ssize_t = __int64;
#define _SSIZE_T_DEFINED
#endif
#endif // _MSC_VER
+#if defined(_MSC_VER) || defined(__MINGW32__)
+#pragma comment(lib, "ws2_32.lib")
+#endif
+
+
#ifndef S_ISREG
#define S_ISREG(m) (((m) & S_IFREG) == S_IFREG)
#endif // S_ISREG
@@ -4528,7 +4531,17 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
@@ -3557,7 +3559,15 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
auto wait_result =
::WaitForSingleObject(event, static_cast<DWORD>(timeout_sec * 1000));
if (wait_result == WAIT_TIMEOUT) {
+#ifdef __MINGW32__
+ typedef INT(WSAAPI * PFN_GETADDRINFOEXCANCEL)(HANDLE * CancelHandle);
+ auto wsdll = LoadLibraryW((wchar_t *)"ws2_32.lib");
+ PFN_GETADDRINFOEXCANCEL GetAddrInfoExCancel =
+ (PFN_GETADDRINFOEXCANCEL)GetProcAddress(wsdll, "GetAddrInfoExCancel");
+ typedef INT (WSAAPI *PFN_GETADDRINFOEXCANCEL)(HANDLE *CancelHandle);
+ auto wsdll = LoadLibraryW((wchar_t*) "ws2_32.lib");
+ PFN_GETADDRINFOEXCANCEL GetAddrInfoExCancel = (PFN_GETADDRINFOEXCANCEL) GetProcAddress(wsdll, "GetAddrInfoExCancel");
+
+ if (cancel_handle) { GetAddrInfoExCancel(&cancel_handle); }
+#else
if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); }
+#endif
+
::CloseHandle(event);
return EAI_AGAIN;
}
@@ -13952,3 +13965,4 @@ inline SSL_CTX *Client::ssl_context() const {
} // namespace httplib
#endif // CPPHTTPLIB_HTTPLIB_H
+
--
2.51.2
2.51.0
@@ -1,26 +0,0 @@
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> {
+5 -17
View File
@@ -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_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R6/lib")
elseif (PLATFORM_NETBSD)
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 -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R7/lib")
endif()
@@ -178,9 +178,7 @@ endif()
# Disable Warnings as Errors for MSVC
if (MSVC AND NOT CXX_CLANG)
# This was dripping into spirv, being overriden, and causing cl flag override warning
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} /W3 /WX-")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
endif()
# Set bundled sdl2/qt as dependent options.
@@ -229,8 +227,6 @@ option(YUZU_DOWNLOAD_ANDROID_VVL "Download validation layer binary for android"
option(YUZU_LEGACY "Apply patches that improve compatibility with older GPUs (e.g. Snapdragon 865) at the cost of performance" OFF)
option(NIGHTLY_BUILD "Use Nightly qualifiers in the update checker and build metadata" OFF)
cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" OFF)
cmake_dependent_option(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF)
@@ -246,12 +242,11 @@ cmake_dependent_option(YUZU_USE_BUNDLED_MOLTENVK "Download bundled MoltenVK lib"
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
set(DEFAULT_ENABLE_OPENSSL ON)
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN OR PLATFORM_OPENBSD)
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN)
# - Windows defaults to the Schannel backend.
# - macOS defaults to the SecureTransport backend.
# - 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.
# - Solaris and OpenBSD have too old backends
# But you can force builds for those platforms to use OpenSSL if you have
# your own copy of it.
set(DEFAULT_ENABLE_OPENSSL OFF)
@@ -264,7 +259,7 @@ endif()
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
if (EXT_DEFAULT OR PLATFORM_SUN OR PLATFORM_OPENBSD)
if (EXT_DEFAULT OR PLATFORM_SUN)
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
endif()
@@ -576,7 +571,6 @@ add_subdirectory(externals)
# pass targets from externals
find_package(enet)
find_package(MbedTLS)
find_package(unordered_dense REQUIRED)
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
find_package(xbyak)
@@ -591,7 +585,6 @@ if (NOT YUZU_STATIC_ROOM)
find_package(sirit)
find_package(gamemode)
find_package(mcl)
find_package(frozen)
if (ARCHITECTURE_riscv64)
find_package(biscuit)
@@ -698,11 +691,6 @@ if (ENABLE_QT)
set(QT_MAJOR_VERSION 6)
# Qt6 sets cxx_std_17 and we need to undo that
set_target_properties(Qt6::Platform PROPERTIES INTERFACE_COMPILE_FEATURES "")
## Qt Externals ##
# QuaZip
AddJsonPackage(quazip)
endif()
if (NOT YUZU_STATIC_ROOM AND NOT (YUZU_USE_BUNDLED_FFMPEG OR YUZU_USE_EXTERNAL_FFMPEG))
+125 -192
View File
@@ -41,11 +41,6 @@ function(cpm_utils_message level name message)
message(${level} "[CPMUtil] ${name}: ${message}")
endfunction()
# propagate a variable to parent scope
macro(Propagate var)
set(${var} ${${var}} PARENT_SCOPE)
endmacro()
function(array_to_list array length out)
math(EXPR range "${length} - 1")
@@ -77,159 +72,6 @@ function(get_json_element object out member default)
set("${out}" "${outvar}" PARENT_SCOPE)
endfunction()
# Determine whether or not a package has a viable system candidate.
function(SystemPackageViable JSON_NAME)
string(JSON object GET "${CPMFILE_CONTENT}" "${JSON_NAME}")
parse_object(${object})
string(REPLACE " " ";" find_args "${find_args}")
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
set(${pkg}_VIABLE ${${package}_FOUND} PARENT_SCOPE)
set(${pkg}_PACKAGE ${package} PARENT_SCOPE)
endfunction()
# Add several packages such that if one is bundled,
# all the rest must also be bundled.
function(AddDependentPackages)
set(_some_system OFF)
set(_some_bundled OFF)
foreach(pkg ${ARGN})
SystemPackageViable(${pkg})
if (${pkg}_VIABLE)
set(_some_system ON)
list(APPEND _system_pkgs ${${pkg}_PACKAGE})
else()
set(_some_bundled ON)
list(APPEND _bundled_pkgs ${${pkg}_PACKAGE})
endif()
endforeach()
if (_some_system AND _some_bundled)
foreach(pkg ${ARGN})
list(APPEND package_names ${${pkg}_PACKAGE})
endforeach()
string(REPLACE ";" ", " package_names "${package_names}")
string(REPLACE ";" ", " bundled_names "${_bundled_pkgs}")
foreach(sys ${_system_pkgs})
list(APPEND system_names ${sys}_FORCE_BUNDLED)
endforeach()
string(REPLACE ";" ", " system_names "${system_names}")
message(FATAL_ERROR "Partial dependency installation detected "
"for the following packages:\n${package_names}\n"
"You can solve this in one of two ways:\n"
"1. Install the following packages to your system if available:"
"\n\t${bundled_names}\n"
"2. Set the following variables to ON:"
"\n\t${system_names}\n"
"This may also be caused by a version mismatch, "
"such as one package being newer than the other.")
endif()
foreach(pkg ${ARGN})
AddJsonPackage(${pkg})
endforeach()
endfunction()
# json util
macro(parse_object object)
get_json_element("${object}" package package ${JSON_NAME})
get_json_element("${object}" repo repo "")
get_json_element("${object}" ci ci OFF)
get_json_element("${object}" version version "")
if(ci)
get_json_element("${object}" name name "${JSON_NAME}")
get_json_element("${object}" extension extension "tar.zst")
get_json_element("${object}" min_version min_version "")
get_json_element("${object}" raw_disabled disabled_platforms "")
if(raw_disabled)
array_to_list("${raw_disabled}"
${raw_disabled_LENGTH} disabled_platforms)
else()
set(disabled_platforms "")
endif()
else()
get_json_element("${object}" hash hash "")
get_json_element("${object}" hash_suffix hash_suffix "")
get_json_element("${object}" sha sha "")
get_json_element("${object}" url url "")
get_json_element("${object}" key key "")
get_json_element("${object}" tag tag "")
get_json_element("${object}" artifact artifact "")
get_json_element("${object}" git_version git_version "")
get_json_element("${object}" git_host git_host "")
get_json_element("${object}" source_subdir source_subdir "")
get_json_element("${object}" bundled bundled "unset")
get_json_element("${object}" find_args find_args "")
get_json_element("${object}" raw_patches patches "")
# okay here comes the fun part: REPLACEMENTS!
# first: tag gets %VERSION% replaced if applicable,
# with either git_version (preferred) or version
# second: artifact gets %VERSION% and %TAG% replaced
# accordingly (same rules for VERSION)
if(git_version)
set(version_replace ${git_version})
else()
set(version_replace ${version})
endif()
# TODO(crueter): fmt module for cmake
if(tag)
string(REPLACE "%VERSION%" "${version_replace}" tag ${tag})
endif()
if(artifact)
string(REPLACE "%VERSION%" "${version_replace}"
artifact ${artifact})
string(REPLACE "%TAG%" "${tag}" artifact ${artifact})
endif()
# format patchdir
if(raw_patches)
math(EXPR range "${raw_patches_LENGTH} - 1")
foreach(IDX RANGE ${range})
string(JSON _patch GET "${raw_patches}" "${IDX}")
set(full_patch
"${PROJECT_SOURCE_DIR}/.patch/${JSON_NAME}/${_patch}")
if(NOT EXISTS ${full_patch})
cpm_utils_message(FATAL_ERROR ${JSON_NAME}
"specifies patch ${full_patch} which does not exist")
endif()
list(APPEND patches "${full_patch}")
endforeach()
endif()
# end format patchdir
# options
get_json_element("${object}" raw_options options "")
if(raw_options)
array_to_list("${raw_options}" ${raw_options_LENGTH} options)
endif()
set(options ${options} ${JSON_OPTIONS})
# end options
# system/bundled
if(bundled STREQUAL "unset" AND DEFINED JSON_BUNDLED_PACKAGE)
set(bundled ${JSON_BUNDLED_PACKAGE})
endif()
endif()
endmacro()
# The preferred usage
function(AddJsonPackage)
set(oneValueArgs
@@ -238,8 +80,7 @@ function(AddJsonPackage)
# these are overrides that can be generated at runtime,
# so can be defined separately from the json
DOWNLOAD_ONLY
BUNDLED_PACKAGE
FORCE_BUNDLED_PACKAGE)
BUNDLED_PACKAGE)
set(multiValueArgs OPTIONS)
@@ -270,9 +111,24 @@ function(AddJsonPackage)
cpm_utils_message(FATAL_ERROR ${JSON_NAME} "Not found in cpmfile")
endif()
parse_object(${object})
get_json_element("${object}" package package ${JSON_NAME})
get_json_element("${object}" repo repo "")
get_json_element("${object}" ci ci OFF)
get_json_element("${object}" version version "")
if(ci)
get_json_element("${object}" name name "${JSON_NAME}")
get_json_element("${object}" extension extension "tar.zst")
get_json_element("${object}" min_version min_version "")
get_json_element("${object}" raw_disabled disabled_platforms "")
if(raw_disabled)
array_to_list("${raw_disabled}"
${raw_disabled_LENGTH} disabled_platforms)
else()
set(disabled_platforms "")
endif()
AddCIPackage(
VERSION ${version}
NAME ${name}
@@ -282,38 +138,116 @@ function(AddJsonPackage)
MIN_VERSION ${min_version}
DISABLED_PLATFORMS ${disabled_platforms})
else()
if (NOT DEFINED JSON_FORCE_BUNDLED_PACKAGE)
set(JSON_FORCE_BUNDLED_PACKAGE OFF)
endif()
# pass stuff to parent scope
set(${package}_ADDED "${${package}_ADDED}"
PARENT_SCOPE)
set(${package}_SOURCE_DIR "${${package}_SOURCE_DIR}"
PARENT_SCOPE)
set(${package}_BINARY_DIR "${${package}_BINARY_DIR}"
PARENT_SCOPE)
AddPackage(
NAME "${package}"
VERSION "${version}"
URL "${url}"
HASH "${hash}"
HASH_SUFFIX "${hash_suffix}"
SHA "${sha}"
REPO "${repo}"
KEY "${key}"
PATCHES "${patches}"
OPTIONS "${options}"
FIND_PACKAGE_ARGUMENTS "${find_args}"
BUNDLED_PACKAGE "${bundled}"
FORCE_BUNDLED_PACKAGE "${JSON_FORCE_BUNDLED_PACKAGE}"
SOURCE_SUBDIR "${source_subdir}"
GIT_VERSION ${git_version}
GIT_HOST ${git_host}
ARTIFACT ${artifact}
TAG ${tag})
return()
endif()
get_json_element("${object}" hash hash "")
get_json_element("${object}" hash_suffix hash_suffix "")
get_json_element("${object}" sha sha "")
get_json_element("${object}" url url "")
get_json_element("${object}" key key "")
get_json_element("${object}" tag tag "")
get_json_element("${object}" artifact artifact "")
get_json_element("${object}" git_version git_version "")
get_json_element("${object}" git_host git_host "")
get_json_element("${object}" source_subdir source_subdir "")
get_json_element("${object}" bundled bundled "unset")
get_json_element("${object}" find_args find_args "")
get_json_element("${object}" raw_patches patches "")
# okay here comes the fun part: REPLACEMENTS!
# first: tag gets %VERSION% replaced if applicable,
# with either git_version (preferred) or version
# second: artifact gets %VERSION% and %TAG% replaced
# accordingly (same rules for VERSION)
if(git_version)
set(version_replace ${git_version})
else()
set(version_replace ${version})
endif()
# TODO(crueter): fmt module for cmake
if(tag)
string(REPLACE "%VERSION%" "${version_replace}" tag ${tag})
endif()
if(artifact)
string(REPLACE "%VERSION%" "${version_replace}" artifact ${artifact})
string(REPLACE "%TAG%" "${tag}" artifact ${artifact})
endif()
# format patchdir
if(raw_patches)
math(EXPR range "${raw_patches_LENGTH} - 1")
foreach(IDX RANGE ${range})
string(JSON _patch GET "${raw_patches}" "${IDX}")
set(full_patch
"${PROJECT_SOURCE_DIR}/.patch/${JSON_NAME}/${_patch}")
if(NOT EXISTS ${full_patch})
cpm_utils_message(FATAL_ERROR ${JSON_NAME}
"specifies patch ${full_patch} which does not exist")
endif()
list(APPEND patches "${full_patch}")
endforeach()
endif()
# end format patchdir
# options
get_json_element("${object}" raw_options options "")
if(raw_options)
array_to_list("${raw_options}" ${raw_options_LENGTH} options)
endif()
set(options ${options} ${JSON_OPTIONS})
# end options
# system/bundled
if(bundled STREQUAL "unset" AND DEFINED JSON_BUNDLED_PACKAGE)
set(bundled ${JSON_BUNDLED_PACKAGE})
endif()
AddPackage(
NAME "${package}"
VERSION "${version}"
URL "${url}"
HASH "${hash}"
HASH_SUFFIX "${hash_suffix}"
SHA "${sha}"
REPO "${repo}"
KEY "${key}"
PATCHES "${patches}"
OPTIONS "${options}"
FIND_PACKAGE_ARGUMENTS "${find_args}"
BUNDLED_PACKAGE "${bundled}"
SOURCE_SUBDIR "${source_subdir}"
GIT_VERSION ${git_version}
GIT_HOST ${git_host}
ARTIFACT ${artifact}
TAG ${tag})
# pass stuff to parent scope
Propagate(${package}_ADDED)
Propagate(${package}_SOURCE_DIR)
Propagate(${package}_BINARY_DIR)
set(${package}_ADDED "${${package}_ADDED}"
PARENT_SCOPE)
set(${package}_SOURCE_DIR "${${package}_SOURCE_DIR}"
PARENT_SCOPE)
set(${package}_BINARY_DIR "${${package}_BINARY_DIR}"
PARENT_SCOPE)
endfunction()
function(AddPackage)
@@ -409,7 +343,7 @@ function(AddPackage)
if(DEFINED PKG_ARGS_ARTIFACT)
set(pkg_url
"${pkg_git_url}/releases/download/${PKG_ARGS_TAG}/${PKG_ARGS_ARTIFACT}")
${pkg_git_url}/releases/download/${PKG_ARGS_TAG}/${PKG_ARGS_ARTIFACT})
else()
set(pkg_url
${pkg_git_url}/archive/refs/tags/${PKG_ARGS_TAG}.tar.gz)
@@ -691,8 +625,7 @@ function(AddCIPackage)
endif()
if (DEFINED pkgname AND NOT "${pkgname}" IN_LIST DISABLED_PLATFORMS)
set(ARTIFACT
"${ARTIFACT_NAME}-${pkgname}-${ARTIFACT_VERSION}.${ARTIFACT_EXT}")
set(ARTIFACT "${ARTIFACT_NAME}-${pkgname}-${ARTIFACT_VERSION}.${ARTIFACT_EXT}")
AddPackage(
NAME ${ARTIFACT_PACKAGE}
+13 -26
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2019 yuzu Emulator Project
@@ -15,40 +15,27 @@ endfunction()
get_timestamp(BUILD_DATE)
if (DEFINED GIT_RELEASE)
set(BUILD_VERSION "${GIT_TAG}")
set(GIT_REFSPEC "${GIT_RELEASE}")
set(IS_DEV_BUILD false)
set(BUILD_VERSION "${GIT_TAG}")
set(GIT_REFSPEC "${GIT_RELEASE}")
set(IS_DEV_BUILD false)
else()
string(SUBSTRING ${GIT_COMMIT} 0 10 BUILD_VERSION)
set(BUILD_VERSION "${BUILD_VERSION}-${GIT_REFSPEC}")
set(IS_DEV_BUILD true)
endif()
if (NIGHTLY_BUILD)
set(IS_NIGHTLY_BUILD true)
else()
set(IS_NIGHTLY_BUILD false)
string(SUBSTRING ${GIT_COMMIT} 0 10 BUILD_VERSION)
set(BUILD_VERSION "${BUILD_VERSION}-${GIT_REFSPEC}")
set(IS_DEV_BUILD true)
endif()
set(GIT_DESC ${BUILD_VERSION})
# Generate cpp with Git revision from template
# Also if this is a CI build, add the build name (ie: Nightly, Canary) to the scm_rev file as well
# Auto-updater metadata! Must somewhat mirror GitHub API endpoint
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
set(BUILD_AUTO_UPDATE_API "http://api.github.com")
if (NIGHTLY_BUILD)
set(BUILD_AUTO_UPDATE_REPO "Eden-CI/Nightly")
set(REPO_NAME "Eden Nightly")
else()
set(BUILD_AUTO_UPDATE_REPO "eden-emulator/Releases")
set(REPO_NAME "Eden")
endif()
set(REPO_NAME "Eden")
set(BUILD_ID ${GIT_REFSPEC})
set(BUILD_FULLNAME "${REPO_NAME} ${BUILD_VERSION} ")
set(CXX_COMPILER "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")
# Auto-updater metadata! Must somewhat mirror GitHub API endpoint
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
set(BUILD_AUTO_UPDATE_API "http://api.github.com")
set(BUILD_AUTO_UPDATE_REPO "eden-emulator/Releases")
configure_file(scm_rev.cpp.in scm_rev.cpp @ONLY)
+7 -18
View File
@@ -12,12 +12,14 @@
"repo": "boostorg/boost",
"tag": "boost-%VERSION%",
"artifact": "%TAG%-cmake.tar.xz",
"hash": "6ae6e94664fe7f2fb01976b59b276ac5df8085c7503fa829d810fbfe495960cfec44fa2c36e2cb23480bc19c956ed199d4952b02639a00a6c07625d4e7130c2d",
"git_version": "1.90.0",
"hash": "4fb7f6fde92762305aad8754d7643cd918dd1f3f67e104e9ab385b18c73178d72a17321354eb203b790b6702f2cf6d725a5d6e2dfbc63b1e35f9eb59fb42ece9",
"git_version": "1.89.0",
"version": "1.57",
"find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem",
"patches": [
"0001-clang-cl.patch"
"0001-clang-cl.patch",
"0002-use-marmasm.patch",
"0003-armasm-options.patch"
]
},
"fmt": {
@@ -46,9 +48,9 @@
"package": "ZLIB",
"repo": "madler/zlib",
"tag": "v%VERSION%",
"hash": "06eaa3a1eaaeb31f461a2283b03a91ed8eb2406e62cd97ea1c69836324909edeecd93edd03ff0bf593d9dde223e3376149134c5b1fe2e8688c258cadf8cd60ff",
"hash": "8c9642495bafd6fad4ab9fb67f09b268c69ff9af0f4f20cf15dfc18852ff1f312bd8ca41de761b3f8d8e90e77d79f2ccacd3d4c5b19e475ecf09d021fdfe9088",
"version": "1.2",
"git_version": "1.3.1.2",
"git_version": "1.3.1",
"options": [
"ZLIB_BUILD_SHARED OFF",
"ZLIB_INSTALL OFF"
@@ -101,18 +103,5 @@
"git_version": "1.4.335.0",
"artifact": "android-binaries-%VERSION%.zip",
"hash": "48167c4a17736301bd08f9290f41830443e1f18cce8ad867fc6f289b49e18b40e93c9850b377951af82f51b5b6d7313aa6a884fc5df79f5ce3df82696c1c1244"
},
"quazip": {
"package": "QuaZip-Qt6",
"repo": "stachenov/quazip",
"sha": "2e95c9001b",
"hash": "609c240c7f029ac26a37d8fbab51bc16284e05e128b78b9b9c0e95d083538c36047a67d682759ac990e4adb0eeb90f04f1ea7fe2253bbda7e7e3bcce32e53dd8",
"version": "1.3",
"git_version": "1.5",
"options": [
"QUAZIP_QT_MAJOR_VERSION 6",
"QUAZIP_INSTALL OFF",
"QUAZIP_ENABLE_QTEXTCODEC OFF"
]
}
}
+1 -1
View File
@@ -18,7 +18,7 @@
src/web_service @AleksandrPopovich
src/dynarmic @Lizzie
src/core @Lizzie @Maufeat @PavelBARABANOV @MrPurple666 @JPikachu
src/core/hle @Maufeat @PavelBARABANOV
src/core/hle @Maufeat @PavelBARABANOV @SDK-Chan
src/core/arm @Lizzie @MrPurple666
src/*_room @AleksandrPopovich
src/video_core @CamilleLaVey @MaranBr @Wildcard @weakboson
-41
View File
@@ -1,41 +0,0 @@
# AddDependentPackage
Use `AddDependentPackage` when you have multiple packages that are required to all be from the system, OR bundled. This is useful in cases where e.g. versions must absolutely match.
## Versioning
Versioning must be handled by the package itself.
## Examples
### Vulkan
`cpmfile.json`
```json
{
"vulkan-headers": {
"repo": "KhronosGroup/Vulkan-Headers",
"package": "VulkanHeaders",
"version": "1.4.317",
"hash": "26e0ad8fa34ab65a91ca62ddc54cc4410d209a94f64f2817dcdb8061dc621539a4262eab6387e9b9aa421db3dbf2cf8e2a4b041b696d0d03746bae1f25191272",
"git_version": "1.4.342",
"tag": "v%VERSION%"
},
"vulkan-utility-libraries": {
"repo": "KhronosGroup/Vulkan-Utility-Libraries",
"package": "VulkanUtilityLibraries",
"hash": "8147370f964fd82c315d6bb89adeda30186098427bf3efaa641d36282d42a263f31e96e4586bfd7ae0410ff015379c19aa4512ba160630444d3d8553afd1ec14",
"git_version": "1.4.342",
"tag": "v%VERSION%"
}
}
```
`CMakeLists.txt`:
```cmake
AddDependentPackages(vulkan-headers vulkan-utility-libraries)
```
If Vulkan Headers are installed, but NOT Vulkan Utility Libraries, then CPMUtil will throw an error.
-4
View File
@@ -31,10 +31,6 @@ The core of CPMUtil is the [`AddPackage`](./AddPackage.md) function. [`AddPackag
[`AddJsonPackage`](./AddJsonPackage.md) is the recommended method of usage for CPMUtil.
## AddDependentPackage
[`AddDependentPackage`](./AddDependentPackage.md) allows you to add multiple packages such that all of them must be from the system OR bundled.
## AddQt
[`AddQt`](./AddQt.md) adds a specific version of Qt to your project.
-2
View File
@@ -85,8 +85,6 @@ If you have `quazip1_qt6_devel`, uninstall it. It may call `Core5Compat` on CMak
## 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`.
`-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
View File
@@ -277,7 +277,7 @@ For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv
```sh
pkg_add -u
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
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
```
[Caveats](./Caveats.md#openbsd).
+5 -5
View File
@@ -82,15 +82,15 @@ You may additionally need the `Qt Extension Pack` extension if building Qt.
# Build speedup
If you have an HDD, use ramdisk (build in RAM), approximatedly you need 4GB for a full build with debug symbols:
If you have an HDD, use ramdisk (build in RAM):
```sh
mkdir /tmp/ramdisk
chmod 777 /tmp/ramdisk
sudo mkdir /tmp/ramdisk
sudo chmod 777 /tmp/ramdisk
# about 8GB needed
mount -t tmpfs -o size=4G myramdisk /tmp/ramdisk
sudo mount -t tmpfs -o size=8G myramdisk /tmp/ramdisk
cmake -B /tmp/ramdisk
cmake --build /tmp/ramdisk -- -j32
umount /tmp/ramdisk
sudo umount /tmp/ramdisk
```
# Assets and large files
+9 -7
View File
@@ -66,9 +66,6 @@ if (NOT TARGET LLVM::Demangle)
add_library(LLVM::Demangle ALIAS demangle)
endif()
# unordered_dense
AddJsonPackage(unordered-dense)
if (YUZU_STATIC_ROOM)
return()
endif()
@@ -86,11 +83,13 @@ endif()
# mcl
AddJsonPackage(mcl)
# Vulkan stuff
AddDependentPackages(vulkan-headers vulkan-utility-libraries)
# VulkanUtilityHeaders - pulls in headers and utility libs
AddJsonPackage(vulkan-utility-headers)
# frozen
AddJsonPackage(frozen)
# small hack
if (NOT VulkanUtilityLibraries_ADDED)
find_package(VulkanHeaders 1.3.274 REQUIRED)
endif()
# DiscordRPC
if (USE_DISCORD_PRESENCE)
@@ -247,6 +246,9 @@ if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER)
AddJsonPackage(cpp-jwt)
endif()
# unordered_dense
AddJsonPackage(unordered-dense)
# FFMpeg
if (YUZU_USE_EXTERNAL_FFMPEG OR YUZU_USE_BUNDLED_FFMPEG)
add_subdirectory(ffmpeg)
+16 -31
View File
@@ -28,8 +28,8 @@
"httplib": {
"repo": "yhirose/cpp-httplib",
"tag": "v%VERSION%",
"hash": "a229e24cca4afe78e5c0aa2e482f15108ac34101fd8dbd927365f15e8c37dec4de38c5277d635017d692a5b320e1b929f8bfcc076f52b8e4dcdab8fe53bfdf2e",
"git_version": "0.30.1",
"hash": "e7a8877d489c97669a8ee536e1498575be921e558ed947253013fe6b67a49d4569eedd01f543caa70183b92d8ac0e8687d662a70d880954412e387317008a239",
"git_version": "0.28.0",
"find_args": "MODULE GLOBAL",
"patches": [
"0001-mingw.patch"
@@ -91,13 +91,10 @@
"unordered-dense": {
"package": "unordered_dense",
"repo": "martinus/unordered_dense",
"sha": "7b55cab841",
"hash": "d2106f6640f6bfb81755e4b8bfb64982e46ec4a507cacdb38f940123212ccf35a20b43c70c6f01d7bfb8c246d1a16f7845d8052971949cea9def1475e3fa02c8",
"tag": "v%VERSION%",
"hash": "b98b5d4d96f8e0081b184d6c4c1181fae4e41723b54bed4296717d7f417348b48fad0bbcc664cac142b8c8a47e95aa57c1eb1cf6caa855fd782fad3e3ab99e5e",
"find_args": "CONFIG",
"bundled": true,
"patches": [
"0001-avoid-memset-when-clearing-an-empty-table.patch"
]
"git_version": "4.8.1"
},
"mbedtls": {
"package": "MbedTLS",
@@ -121,6 +118,15 @@
"git_version": "1.3.18",
"find_args": "MODULE"
},
"vulkan-utility-headers": {
"package": "VulkanUtilityLibraries",
"repo": "scripts/VulkanUtilityHeaders",
"tag": "%VERSION%",
"git_version": "1.4.335",
"artifact": "VulkanUtilityHeaders.tar.zst",
"git_host": "git.crueter.xyz",
"hash": "16dac0e6586702580c4279e4cd37ffe3cf909c93eb31b5069da7af36436d47b270a9cbaac953bb66c22ed12ed67ffa096688599267f307dfb62be1bc09f79833"
},
"spirv-tools": {
"package": "SPIRV-Tools",
"repo": "KhronosGroup/SPIRV-Tools",
@@ -168,9 +174,9 @@
"package": "Catch2",
"repo": "catchorg/Catch2",
"tag": "v%VERSION%",
"hash": "acb3f463a7404d6a3bce52e474075cdadf9bb241d93feaf147c182d756e5a2f8bd412f4658ca186d15ab8fed36fc587d79ec311f55642d8e4ded16df9e213656",
"hash": "a95495142f915d6e9c2a23e80fe360343e9097680066a2f9d3037a070ba5f81ee5559a0407cc9e972dc2afae325873f1fc7ea07a64012c0f01aac6e549f03e3f",
"version": "3.0.1",
"git_version": "3.12.0",
"git_version": "3.11.0",
"patches": [
"0001-solaris-isnan-fix.patch"
]
@@ -275,26 +281,5 @@
"tag": "%VERSION%",
"hash": "dc37a189a44ce8b5c988ca550582431a6c7eadfd3c6e709bee6277116ee803e714333e85c9e6cbb5c69346a14d6f2cc7ed96e8aa09cc5fb8a89f945059651db6",
"version": "121125"
},
"vulkan-headers": {
"repo": "KhronosGroup/Vulkan-Headers",
"package": "VulkanHeaders",
"version": "1.4.317",
"hash": "26e0ad8fa34ab65a91ca62ddc54cc4410d209a94f64f2817dcdb8061dc621539a4262eab6387e9b9aa421db3dbf2cf8e2a4b041b696d0d03746bae1f25191272",
"git_version": "1.4.342",
"tag": "v%VERSION%"
},
"vulkan-utility-libraries": {
"repo": "KhronosGroup/Vulkan-Utility-Libraries",
"package": "VulkanUtilityLibraries",
"hash": "8147370f964fd82c315d6bb89adeda30186098427bf3efaa641d36282d42a263f31e96e4586bfd7ae0410ff015379c19aa4512ba160630444d3d8553afd1ec14",
"git_version": "1.4.342",
"tag": "v%VERSION%"
},
"frozen": {
"package": "frozen",
"repo": "serge-sans-paille/frozen",
"sha": "61dce5ae18",
"hash": "b8dfe741c82bc178dfc9749d4ab5a130cee718d9ee7b71d9b547cf5f7f23027ed0152ad250012a8546399fcc1e12187efc68d89d6731256c4d2df7d04eef8d5c"
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -33,7 +33,7 @@ endif()
if(NOT YUZU_TZDB_PATH STREQUAL "")
set(NX_TZDB_BASE_DIR "${YUZU_TZDB_PATH}")
elseif (MSVC AND NOT CXX_CLANG AND ENABLE_LTO)
elseif (MSVC AND NOT CXX_CLANG AND YUZU_ENABLE_LTO)
# TODO(crueter): boot up the windows vm
set(NX_TZDB_VERSION "250725")
set(NX_TZDB_ARCHIVE "${CPM_SOURCE_CACHE}/nx_tzdb/${NX_TZDB_VERSION}.zip")
+1 -1
View File
@@ -20,4 +20,4 @@ pkgs.mkShellNoCC {
# optional components
discord-rpc gamemode
];
}
}
-4
View File
@@ -20,10 +20,6 @@ if (YUZU_STATIC_BUILD)
add_compile_definitions(QT_STATICPLUGIN)
endif()
if (NIGHTLY_BUILD)
add_compile_definitions(NIGHTLY_BUILD)
endif()
# Set compilation flags
if (MSVC AND NOT CXX_CLANG)
set(CMAKE_CONFIGURATION_TYPES Debug Release CACHE STRING "" FORCE)
+27 -72
View File
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// import android.annotation.SuppressLint
import kotlin.collections.setOf
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
import org.gradle.api.tasks.Copy
@@ -13,8 +14,7 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
id("org.jetbrains.kotlin.plugin.compose")
kotlin("plugin.serialization") version "2.3.0"
kotlin("plugin.serialization") version "1.9.20"
id("androidx.navigation.safeargs.kotlin")
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
id("com.github.triplet.play") version "3.8.6"
@@ -37,12 +37,8 @@ android {
compileSdkVersion = "android-36"
ndkVersion = "28.2.13676358"
val isNightly =
providers.gradleProperty("nightly").orNull?.toBooleanStrictOrNull() ?: false
buildFeatures {
viewBinding = true
compose = true
}
compileOptions {
@@ -50,10 +46,8 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
kotlinOptions {
jvmTarget = "17"
}
packaging {
@@ -67,7 +61,7 @@ android {
defaultConfig {
applicationId = "dev.eden.eden_emulator"
minSdk = 26
minSdk = 24
targetSdk = 36
versionName = getGitVersion()
versionCode = autoVersion
@@ -77,7 +71,6 @@ android {
val extraCMakeArgs =
(project.findProperty("YUZU_ANDROID_ARGS") as String?)?.split("\\s+".toRegex())
?: emptyList()
arguments.addAll(
listOf(
"-DENABLE_QT=0", // Don't use QT
@@ -96,13 +89,6 @@ android {
)
)
if (isNightly) {
arguments.addAll(listOf(
"-DENABLE_UPDATE_CHECKER=ON",
"-DNIGHTLY_BUILD=ON",
))
}
abiFilters("arm64-v8a")
}
}
@@ -139,12 +125,7 @@ android {
signingConfigs.getByName("default")
}
if (isNightly) {
applicationIdSuffix = ".nightly"
manifestPlaceholders += mapOf("appNameSuffix" to " Nightly")
} else {
manifestPlaceholders += mapOf("appNameSuffix" to "")
}
manifestPlaceholders += mapOf("appNameSuffix" to "")
isMinifyEnabled = true
isDebuggable = false
@@ -258,19 +239,6 @@ android {
path = file("${edenDir}/CMakeLists.txt")
}
}
productFlavors.all {
val currentName = manifestPlaceholders["appNameBase"] as? String ?: "Eden"
val suffix = if (isNightly) " Nightly" else ""
// apply nightly suffix I/A
resValue("string", "app_name_suffixed", "$currentName$suffix")
resValue("string", "app_name", "Eden$suffix")
}
}
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
}
idea {
@@ -290,7 +258,7 @@ tasks.register<Delete>("ktlintReset", fun Delete.() {
val showFormatHelp = {
logger.lifecycle(
"If this check fails, please try running \"gradlew ktlintFormat\" for automatic " +
"codestyle fixes"
"codestyle fixes"
)
}
tasks.getByPath("ktlintKotlinScriptCheck").doFirst { showFormatHelp.invoke() }
@@ -298,7 +266,7 @@ tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
ktlint {
version.set("0.50.0")
version.set("0.47.1")
android.set(true)
ignoreFailures.set(false)
disabledRules.set(
@@ -323,42 +291,29 @@ play {
}
dependencies {
implementation("androidx.compose:compose-bom:2026.01.01")
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.core:core-ktx:1.15.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("androidx.documentfile:documentfile:1.1.0")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.fragment:fragment-ktx:1.8.6")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("io.coil-kt:coil:2.7.0")
implementation("androidx.core:core-splashscreen:1.2.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.21.0")
implementation("androidx.window:window:1.5.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0")
implementation("org.commonmark:commonmark:0.27.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.9.7")
implementation("androidx.navigation:navigation-ui-ktx:2.9.7")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
implementation("androidx.window:window:1.3.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("org.commonmark:commonmark:0.22.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.8.9")
implementation("androidx.navigation:navigation-ui-ktx:2.8.9")
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
implementation("androidx.compose.ui:ui-graphics-android:1.10.2")
implementation("androidx.compose.ui:ui-text-android:1.10.2")
implementation("net.swiftzer.semver:semver:2.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("androidx.compose.ui:ui-graphics-android:1.7.8")
implementation("androidx.compose.ui:ui-text-android:1.7.8")
implementation("net.swiftzer.semver:semver:2.0.0")
}
fun runGitCommand(command: List<String>): String {
-230
View File
@@ -1,230 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 13 KiB

@@ -1,184 +0,0 @@
// 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),
)
}
}
}
@@ -1,157 +0,0 @@
// 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,
)
}
}
@@ -1,259 +0,0 @@
// 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)
)
}
}
}
}
}
@@ -1,319 +0,0 @@
// 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,
)
}
}
}
}
}
@@ -1,304 +0,0 @@
// 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
@@ -1,30 +0,0 @@
// 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,
)
}
}
@@ -1,66 +0,0 @@
// 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)
@@ -1,186 +0,0 @@
// 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,
)
@@ -1,80 +0,0 @@
// 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,
)
}
}
}
@@ -1,82 +0,0 @@
// 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,
)
}
}
}
@@ -1,93 +0,0 @@
// 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
}
}
@@ -1,59 +0,0 @@
// 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,
)
@@ -1,42 +0,0 @@
// 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,
)
@@ -1,42 +0,0 @@
// 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)
}
@@ -1,89 +0,0 @@
// 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)
@@ -1,17 +0,0 @@
// 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)
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -218,7 +218,7 @@ object NativeLibrary {
/**
* Checks for available updates.
*/
external fun checkForUpdate(): Array<String>?
external fun checkForUpdate(): String?
/**
* Return the URL to the release page
@@ -228,18 +228,13 @@ object NativeLibrary {
/**
* Return the URL to download the APK for the given version
*/
external fun getUpdateApkUrl(tag: String, artifact: String, packageId: String): String
external fun getUpdateApkUrl(version: String, packageId: String): String
/**
* Returns whether the update checker is enabled through CMAKE options.
*/
external fun isUpdateCheckerEnabled(): Boolean
/**
* Returns whether or not this is a nightly build.
*/
external fun isNightlyBuild(): Boolean
/**
* Returns the build version generated by CMake (BUILD_VERSION).
*/
@@ -614,23 +609,4 @@ object NativeLibrary {
* Updates the device power state to global variables
*/
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 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -18,8 +18,6 @@ import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import android.content.res.Configuration
import android.os.LocaleList
import coil.ImageLoader
import coil.ImageLoaderFactory
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DocumentsTree
@@ -30,7 +28,7 @@ import java.util.Locale
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
class YuzuApplication : Application(), ImageLoaderFactory {
class YuzuApplication : Application() {
private fun createNotificationChannels() {
val name: CharSequence = getString(R.string.app_notification_channel_name)
val description = getString(R.string.app_notification_channel_description)
@@ -78,12 +76,6 @@ class YuzuApplication : Application(), ImageLoaderFactory {
createNotificationChannels()
}
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.crossfade(true)
.build()
}
companion object {
var documentsTree: DocumentsTree? = null
lateinit var application: YuzuApplication
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -649,7 +649,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
fun launch(activity: AppCompatActivity, game: Game) {
val launcher = Intent(activity, EmulationActivity::class.java)
launcher.putExtra("game", game) // Use "game" to match navigation argus
launcher.putExtra(EXTRA_SELECTED_GAME, game)
activity.startActivity(launcher)
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -10,7 +10,6 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
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.viewholder.AbstractViewHolder
@@ -32,12 +31,7 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
binding.addonSwitch.isChecked = model.enabled
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
if (PatchType.from(model.type) == PatchType.Update && checked) {
addonViewModel.enableOnlyThisUpdate(model)
notifyDataSetChanged()
} else {
model.enabled = checked
}
model.enabled = checked
}
val deleteAction = {
@@ -1,55 +0,0 @@
// 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,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -10,10 +7,8 @@ import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
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.GamesViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
@@ -36,12 +31,6 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
path.text = Uri.parse(model.uriString).path
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 {
GameFolderPropertiesDialogFragment.newInstance(model)
.show(
@@ -1,112 +0,0 @@
// 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))
}
}
}
}
@@ -22,6 +22,14 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
class QuickSettings(val emulationFragment: EmulationFragment) {
// Kinda a crappy workaround to get a title from setting keys
// Idk how to do this witthout hardcoding every single one
private fun getSettingTitle(settingKey: String): String {
return settingKey.replace("_", " ").split(" ")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
}
private fun saveSettings() {
if (emulationFragment.shouldUseCustom) {
NativeConfig.savePerGameConfig()
@@ -42,7 +50,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
statusText.setTextColor(
MaterialColors.getColor(
statusText,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
@@ -52,7 +60,6 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
// settings
fun addIntSetting(
name: Int,
container: ViewGroup,
setting: IntSetting,
namesArrayId: Int,
@@ -66,7 +73,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.expand_icon)
val radioGroup = itemView.findViewById<RadioGroup>(R.id.radio_group)
titleView.text = YuzuApplication.appContext.getString(name)
titleView.text = getSettingTitle(setting.key)
val names = emulationFragment.resources.getStringArray(namesArrayId)
val values = emulationFragment.resources.getIntArray(valuesArrayId)
@@ -108,8 +115,6 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
}
fun addBooleanSetting(
name: Int,
container: ViewGroup,
setting: BooleanSetting
) {
@@ -120,7 +125,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
val titleView = itemView.findViewById<TextView>(R.id.switch_title)
val switchView = itemView.findViewById<com.google.android.material.materialswitch.MaterialSwitch>(R.id.setting_switch)
titleView.text = YuzuApplication.appContext.getString(name)
titleView.text = getSettingTitle(setting.key)
switchContainer.visibility = View.VISIBLE
switchView.isChecked = setting.getBoolean()
@@ -136,7 +141,6 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
}
fun addSliderSetting(
name: Int,
container: ViewGroup,
setting: AbstractSetting,
minValue: Int = 0,
@@ -152,7 +156,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
val slider = itemView.findViewById<com.google.android.material.slider.Slider>(R.id.setting_slider)
titleView.text = YuzuApplication.appContext.getString(name)
titleView.text = getSettingTitle(setting.key)
sliderContainer.visibility = View.VISIBLE
slider.valueFrom = minValue.toFloat()
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.fetcher
@@ -171,7 +171,7 @@ class ReleaseAdapter(
iconTint = ColorStateList.valueOf(
MaterialColors.getColor(
this,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
@@ -24,7 +24,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
ENABLE_BUFFER_HISTORY("enable_buffer_history"),
SYNC_MEMORY_OPERATIONS("sync_memory_operations"),
BUFFER_REORDER_DISABLE("disable_buffer_reorder"),
RENDERER_DEBUG("debug"),
@@ -37,9 +36,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),
ENABLE_FOLDER_BUTTON("enable_folder_button"),
ENABLE_QLAUNCH_BUTTON("enable_qlaunch_button"),
ENABLE_UPDATE_CHECKS("enable_update_checks"),
JOYSTICK_REL_CENTER("joystick_rel_center"),
DPAD_SLIDE("dpad_slide"),
@@ -73,17 +69,11 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
SHOW_SHADERS_BUILDING("show_shaders_building"),
DEBUG_FLUSH_BY_LINE("flush_line"),
USE_LRU_CACHE("use_lru_cache"),
DONT_SHOW_DRIVER_SHADER_WARNING("dont_show_driver_shader_warning"),
ENABLE_OVERLAY("enable_overlay"),
ENABLE_OVERLAY("enable_overlay");
// GPU Logging
GPU_LOGGING_ENABLED("gpu_logging_enabled"),
GPU_LOG_VULKAN_CALLS("gpu_log_vulkan_calls"),
GPU_LOG_SHADER_DUMPS("gpu_log_shader_dumps"),
GPU_LOG_MEMORY_TRACKING("gpu_log_memory_tracking"),
GPU_LOG_DRIVER_DEBUG("gpu_log_driver_debug"),
ENABLE_QUICK_SETTINGS("enable_quick_settings");
// external fun isFrameSkippingEnabled(): Boolean
external fun isFrameInterpolationEnabled(): Boolean
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -9,8 +9,7 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ByteSetting(override val key: String) : AbstractByteSetting {
AUDIO_VOLUME("volume"),
GPU_LOG_LEVEL("gpu_log_level");
AUDIO_VOLUME("volume"),;
override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
@@ -67,8 +67,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
MY_PAGE_APPLET("my_page_applet_mode"),
INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"),
OVERLAY_GRID_SIZE("overlay_grid_size"),
DEBUG_KNOBS("debug_knobs"),
GPU_LOG_RING_BUFFER_SIZE("gpu_log_ring_buffer_size")
DEBUG_KNOBS("debug_knobs")
;
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
@@ -60,11 +60,6 @@ abstract class SettingsItem(
return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
}
// Can't edit enable_qlaunch_button if firmware is not available
if (setting.key == BooleanSetting.ENABLE_QLAUNCH_BUTTON.key) {
return NativeLibrary.isFirmwareAvailable()
}
// Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false
@@ -123,6 +118,13 @@ abstract class SettingsItem(
// List of all general
val settingsItems = HashMap<String, SettingsItem>().apply {
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(
SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
@@ -738,13 +740,6 @@ abstract class SettingsItem(
descriptionId = R.string.renderer_reactive_flushing_description
)
)
put(
SwitchSetting(
BooleanSetting.ENABLE_BUFFER_HISTORY,
titleId = R.string.enable_buffer_history,
descriptionId = R.string.enable_buffer_history_description
)
)
put(
SwitchSetting(
BooleanSetting.SYNC_MEMORY_OPERATIONS,
@@ -799,27 +794,6 @@ abstract class SettingsItem(
descriptionId = R.string.enable_update_checks_description,
)
)
put(
SwitchSetting(
BooleanSetting.ENABLE_QUICK_SETTINGS,
titleId = R.string.enable_quick_settings,
descriptionId = R.string.enable_quick_settings_description,
)
)
put(
SwitchSetting(
BooleanSetting.ENABLE_FOLDER_BUTTON,
titleId = R.string.enable_folder_button,
descriptionId = R.string.enable_folder_button_description,
)
)
put(
SwitchSetting(
BooleanSetting.ENABLE_QLAUNCH_BUTTON,
titleId = R.string.enable_qlaunch_button,
descriptionId = R.string.enable_qlaunch_button_description,
)
)
put(
SingleChoiceSetting(
IntSetting.APP_LANGUAGE,
@@ -862,62 +836,6 @@ abstract class SettingsItem(
)
)
// GPU Logging settings
put(
SwitchSetting(
BooleanSetting.GPU_LOGGING_ENABLED,
titleId = R.string.gpu_logging_enabled,
descriptionId = R.string.gpu_logging_enabled_description
)
)
put(
SingleChoiceSetting(
ByteSetting.GPU_LOG_LEVEL,
titleId = R.string.gpu_log_level,
descriptionId = R.string.gpu_log_level_description,
choicesId = R.array.gpuLogLevelEntries,
valuesId = R.array.gpuLogLevelValues
)
)
put(
SwitchSetting(
BooleanSetting.GPU_LOG_VULKAN_CALLS,
titleId = R.string.gpu_log_vulkan_calls,
descriptionId = R.string.gpu_log_vulkan_calls_description
)
)
put(
SwitchSetting(
BooleanSetting.GPU_LOG_SHADER_DUMPS,
titleId = R.string.gpu_log_shader_dumps,
descriptionId = R.string.gpu_log_shader_dumps_description
)
)
put(
SwitchSetting(
BooleanSetting.GPU_LOG_MEMORY_TRACKING,
titleId = R.string.gpu_log_memory_tracking,
descriptionId = R.string.gpu_log_memory_tracking_description
)
)
put(
SwitchSetting(
BooleanSetting.GPU_LOG_DRIVER_DEBUG,
titleId = R.string.gpu_log_driver_debug,
descriptionId = R.string.gpu_log_driver_debug_description
)
)
put(
SpinBoxSetting(
IntSetting.GPU_LOG_RING_BUFFER_SIZE,
titleId = R.string.gpu_log_ring_buffer_size,
descriptionId = R.string.gpu_log_ring_buffer_size_description,
valueHint = R.string.gpu_log_ring_buffer_size_hint,
min = 64,
max = 4096
)
)
val fastmem = object : AbstractBooleanSetting {
override fun getBoolean(needsGlobal: Boolean): Boolean =
BooleanSetting.FASTMEM.getBoolean() &&
@@ -235,6 +235,7 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.cpu))
add(IntSetting.FAST_CPU_TIME.key)
add(BooleanSetting.USE_LRU_CACHE.key)
add(BooleanSetting.CORE_SYNC_CORE_SPEED.key)
add(IntSetting.MEMORY_LAYOUT.key)
@@ -274,7 +275,6 @@ class SettingsFragmentPresenter(
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
add(BooleanSetting.ENABLE_BUFFER_HISTORY.key)
add(HeaderSetting(R.string.hacks))
@@ -1074,8 +1074,6 @@ class SettingsFragmentPresenter(
add(BooleanSetting.ENABLE_UPDATE_CHECKS.key)
}
add(BooleanSetting.ENABLE_QUICK_SETTINGS.key)
add(HeaderSetting(R.string.theme_and_color))
@@ -1193,13 +1191,6 @@ class SettingsFragmentPresenter(
descriptionId = R.string.use_black_backgrounds_description
)
)
add(HeaderSetting(R.string.buttons))
add(BooleanSetting.ENABLE_FOLDER_BUTTON.key)
add(BooleanSetting.ENABLE_QLAUNCH_BUTTON.key)
if (!NativeLibrary.isFirmwareAvailable()) {
BooleanSetting.ENABLE_QLAUNCH_BUTTON.setBoolean(false)
}
}
}
@@ -1229,15 +1220,6 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.general))
add(IntSetting.DEBUG_KNOBS.key)
add(HeaderSetting(R.string.gpu_logging_header))
add(BooleanSetting.GPU_LOGGING_ENABLED.key)
add(ByteSetting.GPU_LOG_LEVEL.key)
add(BooleanSetting.GPU_LOG_VULKAN_CALLS.key)
add(BooleanSetting.GPU_LOG_SHADER_DUMPS.key)
add(BooleanSetting.GPU_LOG_MEMORY_TRACKING.key)
add(BooleanSetting.GPU_LOG_DRIVER_DEBUG.key)
add(IntSetting.GPU_LOG_RING_BUFFER_SIZE.key)
}
}
@@ -1,459 +0,0 @@
// 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
}
}
@@ -690,18 +690,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
})
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
if (!BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()) {
binding.drawerLayout.setDrawerLockMode(
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
binding.quickSettingsSheet
)
}
updateGameTitle()
binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible =
BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()
binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
val lockMode = IntSetting.LOCK_DRAWER.getInt()
val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
@@ -759,11 +749,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true
}
if (BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean())
R.id.menu_quick_settings else 0 -> {
openQuickSettingsMenu()
true
}
R.id.menu_quick_settings -> {
openQuickSettingsMenu()
true
}
R.id.menu_settings_per_game -> {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
@@ -1056,13 +1045,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
quickSettings.addBooleanSetting(
R.string.frame_limit_enable,
container,
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
)
quickSettings.addSliderSetting(
R.string.frame_limit_slider,
container,
ShortSetting.RENDERER_SPEED_LIMIT,
minValue = 0,
@@ -1071,7 +1058,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
)
quickSettings.addBooleanSetting(
R.string.use_docked_mode,
container,
BooleanSetting.USE_DOCKED_MODE,
)
@@ -1079,7 +1065,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
quickSettings.addDivider(container)
quickSettings.addIntSetting(
R.string.renderer_accuracy,
container,
IntSetting.RENDERER_ACCURACY,
R.array.rendererAccuracyNames,
@@ -1088,7 +1073,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
quickSettings.addIntSetting(
R.string.renderer_scaling_filter,
container,
IntSetting.RENDERER_SCALING_FILTER,
R.array.rendererScalingFilterNames,
@@ -1096,7 +1080,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
)
quickSettings.addSliderSetting(
R.string.fsr_sharpness,
container,
IntSetting.FSR_SHARPENING_SLIDER,
minValue = 0,
@@ -1105,7 +1088,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
)
quickSettings.addIntSetting(
R.string.renderer_anti_aliasing,
container,
IntSetting.RENDERER_ANTI_ALIASING,
R.array.rendererAntiAliasingNames,
@@ -1310,7 +1292,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
}
@@ -1497,7 +1479,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
androidx.appcompat.R.attr.colorPrimary
com.google.android.material.R.attr.colorPrimary
)
)
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -110,7 +110,7 @@ class FreedrenoSettingsFragment : Fragment() {
val commonVars = listOf(
"TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
"FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
"TU_BREADCRUMBS", "FD_DEV_FEATURES"
"TU_BREADCRUMBS"
)
for (varName in commonVars) {
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -9,13 +6,11 @@ package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
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.GamesViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig
@@ -30,18 +25,14 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
// Hide deepScan for external content, do automatically
if (gameDir.type == DirectoryType.EXTERNAL_CONTENT) {
binding.deepScanSwitch.visibility = View.GONE
} else {
// Restore checkbox state for game dirs
binding.deepScanSwitch.isChecked =
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
// Restore checkbox state
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
binding.deepScanSwitch.setOnClickListener {
deepScan = binding.deepScanSwitch.isChecked
}
}
return MaterialAlertDialogBuilder(requireContext())
@@ -50,10 +41,8 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
if (folderIndex != -1) {
if (gameDir.type == DirectoryType.GAME) {
gamesViewModel.folders.value[folderIndex].deepScan =
binding.deepScanSwitch.isChecked
}
gamesViewModel.folders.value[folderIndex].deepScan =
binding.deepScanSwitch.isChecked
gamesViewModel.updateGameDirs()
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -15,14 +15,11 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.FolderAdapter
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.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
@@ -76,25 +73,7 @@ class GameFoldersFragment : Fragment() {
val mainActivity = requireActivity() as MainActivity
binding.buttonAdd.setOnClickListener {
// 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()
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
setInsets()
@@ -1,7 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -117,17 +114,6 @@ 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(
HomeSetting(
R.string.gpu_driver_manager,
@@ -236,14 +222,6 @@ class HomeSettingsFragment : Fragment() {
{ shareLog() }
)
)
add(
HomeSetting(
R.string.share_gpu_log,
R.string.share_gpu_log_description,
R.drawable.ic_log,
{ shareGpuLog() }
)
)
add(
HomeSetting(
R.string.open_user_folder,
@@ -430,40 +408,6 @@ class HomeSettingsFragment : Fragment() {
}
}
private fun shareGpuLog() {
val currentLog = DocumentFile.fromSingleUri(
mainActivity,
DocumentsContract.buildDocumentUri(
DocumentProvider.AUTHORITY,
"${DocumentProvider.ROOT_ID}/log/eden_gpu.log"
)
)!!
val oldLog = DocumentFile.fromSingleUri(
mainActivity,
DocumentsContract.buildDocumentUri(
DocumentProvider.AUTHORITY,
"${DocumentProvider.ROOT_ID}/log/eden_gpu.log.old.txt"
)
)!!
val intent = Intent(Intent.ACTION_SEND)
.setDataAndType(currentLog.uri, FileUtil.TEXT_PLAIN)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (!Log.gameLaunched && oldLog.exists()) {
intent.putExtra(Intent.EXTRA_STREAM, oldLog.uri)
startActivity(Intent.createChooser(intent, getText(R.string.share_gpu_log)))
} else if (currentLog.exists()) {
intent.putExtra(Intent.EXTRA_STREAM, currentLog.uri)
startActivity(Intent.createChooser(intent, getText(R.string.share_gpu_log)))
} else {
Toast.makeText(
requireContext(),
getText(R.string.share_gpu_log_missing),
Toast.LENGTH_SHORT
).show()
}
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
@@ -1,190 +0,0 @@
// 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,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -51,68 +48,16 @@ class AddonViewModel : ViewModel() {
?: emptyArray()
).toMutableList()
patchList.sortBy { it.name }
// Ensure only one update is enabled
ensureSingleUpdateEnabled(patchList)
removeDuplicates(patchList)
_patchList.value = patchList
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?) {
_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) {
when (PatchType.from(patch.type)) {
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
@@ -127,27 +72,13 @@ class AddonViewModel : ViewModel() {
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(
game!!.programId,
_patchList.value.mapNotNull {
if (it.enabled) {
null
} else {
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
}
it.name
}
}.toTypedArray()
)
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -12,14 +9,5 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class GameDir(
val uriString: String,
var deepScan: Boolean,
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
}
var deepScan: Boolean
) : Parcelable
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model
@@ -56,31 +56,21 @@ class GamesViewModel : ViewModel() {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
getGameDirsAndExternalContent()
getGameDirs()
reloadGames(directoriesChanged = false, firstStartup = true)
}
fun setGames(games: List<Game>) {
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
val sortedList = games.sortedWith(
compareByDescending<Game> { game ->
preferences.getLong(game.keyLastPlayedTime, 0L)
}.thenBy { it.title.lowercase(Locale.getDefault()) }
.thenBy { it.path }
compareBy(
{ it.title.lowercase(Locale.getDefault()) },
{ it.path }
)
)
_games.value = sortedList
}
fun resortGames() {
val currentGames = _games.value
if (currentGames.isNotEmpty()) {
setGames(currentGames)
_shouldScrollToTop.value = true
}
}
fun setShouldSwapData(shouldSwap: Boolean) {
_shouldSwapData.value = shouldSwap
}
@@ -154,19 +144,11 @@ class GamesViewModel : ViewModel() {
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
viewModelScope.launch {
withContext(Dispatchers.IO) {
when (gameDir.type) {
DirectoryType.GAME -> {
NativeConfig.addGameDir(gameDir)
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.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()
}
}
NativeConfig.addGameDir(gameDir)
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
getGameDirs(!isFirstTimeSetup)
}
if (savedFromGameFragment) {
@@ -186,15 +168,8 @@ class GamesViewModel : ViewModel() {
val removedDirIndex = gameDirs.indexOf(gameDir)
if (removedDirIndex != -1) {
gameDirs.removeAt(removedDirIndex)
when (gameDir.type) {
DirectoryType.GAME -> {
NativeConfig.setGameDirs(gameDirs.filter { it.type == DirectoryType.GAME }.toTypedArray())
}
DirectoryType.EXTERNAL_CONTENT -> {
removeExternalContentDir(gameDir.uriString)
}
}
getGameDirsAndExternalContent()
NativeConfig.setGameDirs(gameDirs.toTypedArray())
getGameDirs()
}
}
}
@@ -202,16 +177,15 @@ class GamesViewModel : ViewModel() {
fun updateGameDirs() =
viewModelScope.launch {
withContext(Dispatchers.IO) {
val gameDirs = _folders.value.filter { it.type == DirectoryType.GAME }
NativeConfig.setGameDirs(gameDirs.toTypedArray())
getGameDirsAndExternalContent()
NativeConfig.setGameDirs(_folders.value.toTypedArray())
getGameDirs()
}
}
fun onOpenGameFoldersFragment() =
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirsAndExternalContent()
getGameDirs()
}
}
@@ -219,36 +193,16 @@ class GamesViewModel : ViewModel() {
NativeConfig.saveGlobalConfig()
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirsAndExternalContent(true)
getGameDirs(true)
}
}
}
private fun getGameDirsAndExternalContent(reloadList: Boolean = false) {
val gameDirs = NativeConfig.getGameDirs().toMutableList()
val externalContentDirs = NativeConfig.getExternalContentDirs().map {
GameDir(it, false, DirectoryType.EXTERNAL_CONTENT)
}
gameDirs.addAll(externalContentDirs)
_folders.value = gameDirs
private fun getGameDirs(reloadList: Boolean = false) {
val gameDirs = NativeConfig.getGameDirs()
_folders.value = gameDirs.toMutableList()
if (reloadList) {
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,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -15,6 +12,5 @@ data class Patch(
val version: String,
val type: Int,
val programId: String,
val titleId: String,
val numericVersion: Long = 0
val titleId: String
)
@@ -1,21 +0,0 @@
// 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("-", "")
}
}
@@ -1,470 +1,508 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
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.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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 android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import java.util.Locale
import androidx.preference.PreferenceManager
import kotlinx.coroutines.delay
import org.yuzu.yuzu_emu.NativeLibrary
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.GameAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import dev.eden.emu.ui.background.RetroGridBackground
import dev.eden.emu.ui.components.FooterAction
import dev.eden.emu.ui.components.FooterButton
import dev.eden.emu.ui.components.GameCarousel
import dev.eden.emu.ui.components.GameGrid
import dev.eden.emu.ui.components.GameTile
import dev.eden.emu.ui.components.HeaderFocusRequesters
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
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler
import java.util.Locale
import androidx.core.content.edit
import androidx.core.view.doOnNextLayout
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() {
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 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(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
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)
}
)
}
}
}
_binding = FragmentGamesBinding.inflate(inflater)
return binding.root
}
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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()
binding.addDirectory.setOnClickListener {
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
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() {
super.onResume()
// Only resort games if a game was actually launched (not when coming back from settings)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
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
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
(binding.gridGames as? CarouselRecyclerView)?.setupCarousel(true)
(binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition)
}
}
// 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) }
private var lastSearchText: String = ""
private var lastFilter: Int = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
// Game properties screen state
var selectedGameTile by remember { mutableStateOf<GameTile?>(null) }
private fun setAdapter(games: List<Game>) {
val currentSearchText = binding.searchText.text.toString()
val currentFilter = binding.filterButton.id
// 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) }
val searchChanged = currentSearchText != lastSearchText
val filterChanged = currentFilter != lastFilter
// 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
if (searchChanged || filterChanged) {
filterAndSearch(games)
lastSearchText = currentSearchText
lastFilter = currentFilter
} 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()) }
}
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
gamesViewModel.setFilteredGames(games)
}
}
val gameTiles = remember(filteredGames) {
filteredGames.map { game ->
GameTile.fromGame(game)
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() }
}
// NO key handling at top level
Box(modifier = Modifier.fillMaxSize()) {
RetroGridBackground()
private fun navigateToSettings() {
val navController = findNavController()
navController.navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
}
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),
)
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
}
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),
)
R.id.view_grid_compact -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID_COMPACT)
applyGridGamesBinding()
item.isChecked = true
true
}
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),
)
R.id.view_list -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_LIST)
applyGridGamesBinding()
item.isChecked = true
true
}
}
// 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
}
R.id.view_carousel -> {
if (!item.isChecked || getCurrentViewType() != GameAdapter.VIEW_TYPE_CAROUSEL) {
setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL)
applyGridGamesBinding()
item.isChecked = true
onResume()
}
) {
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,
)
}
true
}
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)),
)
}
else -> false
}
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 },
)
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
}
// Sort selection dialog
if (showSortDialog) {
SortSelectionDialog(
currentSortMode = sortMode,
onDismiss = { showSortDialog = false },
onSelectSort = { selectedSortMode ->
sortMode = selectedSortMode
gamesViewModel.resortGames()
},
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 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
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
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.ui.main
@@ -63,7 +63,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.documentfile.provider.DocumentFile
class MainActivity : AppCompatActivity(), ThemeProvider {
private lateinit var binding: ActivityMainBinding
@@ -124,14 +123,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
WindowCompat.setDecorFitsSystemWindows(window, false)
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 =
ContextCompat.getColor(applicationContext, android.R.color.transparent)
window.navigationBarColor =
@@ -192,25 +183,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val latestVersion = NativeLibrary.checkForUpdate()
if (latestVersion != null) {
runOnUiThread {
val tag: String = latestVersion[0]
val name: String = latestVersion[1]
showUpdateDialog(tag, name)
showUpdateDialog(latestVersion)
}
}
}.start()
}
private fun showUpdateDialog(tag: String, name: String) {
private fun showUpdateDialog(version: String) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.update_available)
.setMessage(getString(R.string.update_available_description, name))
.setMessage(getString(R.string.update_available_description, version))
.setPositiveButton(android.R.string.ok) { _, _ ->
var artifact = tag
// Nightly builds have a slightly different format
if (NativeLibrary.isNightlyBuild()) {
artifact = tag.substringAfter('.', tag)
}
downloadAndInstallUpdate(tag, artifact)
downloadAndInstallUpdate(version)
}
.setNeutralButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
@@ -223,11 +207,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
.show()
}
private fun downloadAndInstallUpdate(version: String, artifact: String) {
private fun downloadAndInstallUpdate(version: String) {
CoroutineScope(Dispatchers.IO).launch {
val packageId = applicationContext.packageName
val apkUrl = NativeLibrary.getUpdateApkUrl(version, artifact, packageId)
val apkFile = File(cacheDir, "update-$artifact.apk")
val apkUrl = NativeLibrary.getUpdateApkUrl(version, packageId)
val apkFile = File(cacheDir, "update-$version.apk")
withContext(Dispatchers.Main) {
showDownloadProgressDialog()
@@ -397,13 +381,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}
val getExternalContentDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
processExternalContentDir(result)
}
}
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
contentResolver.takePersistableUriPermission(
result,
@@ -425,27 +402,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
.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 ->
if (result != null) {
processKey(result, "keys")
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -25,7 +25,6 @@ object DirectoryInitialization {
initializeInternalStorage()
NativeLibrary.initializeSystem(false)
NativeConfig.initializeGlobalConfig()
NativeLibrary.reloadProfiles()
migrateSettings()
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-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
import android.content.SharedPreferences
@@ -49,17 +49,6 @@ object GameHelper {
// Remove previous filesystem provider information so we can get up to date version info
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>()
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
val gameDirUri = gameDir.uriString.toUri()
@@ -99,33 +88,6 @@ object GameHelper {
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(
games: MutableList<Game>,
files: Array<MinimalDocumentFile>,
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -28,29 +25,31 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
class GameObjectIconFetcher(
class GameIconFetcher(
private val game: Game,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult {
val bitmap = decodeGameIcon(game.path)
?: throw IllegalStateException("Failed to decode game icon for: ${game.title}")
return DrawableResult(
drawable = bitmap.toDrawable(options.context.resources),
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.DISK
)
}
private fun decodeGameIcon(path: String): Bitmap? {
val iconBytes = GameMetadata.getIcon(path)
return BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
private fun decodeGameIcon(uri: String): Bitmap? {
val data = GameMetadata.getIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
}
class Factory : Fetcher.Factory<Game> {
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
GameObjectIconFetcher(data, options)
GameIconFetcher(data, options)
}
}
@@ -62,7 +61,7 @@ object GameIconUtils {
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
.components {
add(GameIconKeyer())
add(GameObjectIconFetcher.Factory())
add(GameIconFetcher.Factory())
}
.memoryCache {
MemoryCache.Builder(YuzuApplication.appContext)
@@ -100,6 +99,9 @@ object GameIconUtils {
R.id.shortcut_foreground,
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(
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
)
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -204,12 +204,4 @@ object NativeConfig {
external fun getSdmcDir(): String
@Synchronized
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 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -144,15 +144,6 @@ 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(
name = "Performance - Default",
description = "Clear all debug options for performance",
@@ -168,7 +159,6 @@ object FreedrenoPresets {
CAPTURE_FRAMES,
SHADER_DEBUG,
GPU_HANG_TRACE,
DEV_FEATURES_UBWC_HINT,
PERFORMANCE_DEFAULT
)
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -74,7 +74,7 @@ class GradientBorderCardView @JvmOverloads constructor(
borderPaint.shader = null
val typedValue = android.util.TypedValue()
context.theme.resolveAttribute(
androidx.appcompat.R.attr.colorPrimary,
com.google.android.material.R.attr.colorPrimary,
typedValue,
true
)
@@ -1,9 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <common/fs/path_util.h>
#include <common/logging/log.h>
#include <common/settings.h>
#include <input_common/main.h>
#include "android_config.h"
#include "android_settings.h"
@@ -70,18 +69,6 @@ void AndroidConfig::ReadPathValues() {
}
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"));
if (!nand_dir_setting.empty()) {
Common::FS::SetEdenPath(Common::FS::EdenPath::NANDDir, nand_dir_setting);
@@ -254,14 +241,6 @@ void AndroidConfig::SavePathValues() {
}
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
const auto nand_path = Common::FS::GetEdenPathString(Common::FS::EdenPath::NANDDir);
WriteStringSetting(std::string("nand_directory"), nand_path,
@@ -54,6 +54,10 @@ namespace AndroidSettings {
Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
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_mode{linkage, -1, "theme_mode", Settings::Category::Android};
Settings::Setting<bool> black_backgrounds{linkage, false, "black_backgrounds",
@@ -61,10 +65,6 @@ namespace AndroidSettings {
Settings::Setting<s32> app_language{linkage, 0, "app_language", Settings::Category::Android};
Settings::Setting<bool> enable_update_checks{linkage, true, "enable_update_checks",
Settings::Category::Android};
Settings::Setting<bool> enable_folder_button{linkage, true, "enable_folder_button",
Settings::Category::Android};
Settings::Setting<bool> enable_qlaunch_button{linkage, false, "enable_qlaunch_button",
Settings::Category::Android};
// Input/performance overlay settings
std::vector<OverlayControlData> overlay_control_data;
@@ -198,14 +198,9 @@ namespace AndroidSettings {
Settings::Specialization::Default, true, true,
&show_soc_overlay};
// MISC
Settings::Setting<bool> dont_show_driver_shader_warning{linkage, false,
"dont_show_driver_shader_warning",
Settings::Category::Android, Settings::Specialization::Default, true, true};
Settings::Setting<bool> enable_quick_settings{linkage, true,
"enable_quick_settings",
Settings::Category::Android, Settings::Specialization::Default, true,
false};
};
extern Values values;
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -21,7 +18,7 @@ struct RomMetadata {
bool isHomebrew;
};
ankerl::unordered_dense::map<std::string, RomMetadata> m_rom_metadata_cache;
std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
RomMetadata CacheRomMetadata(const std::string& path) {
const auto file =
+29 -534
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -49,17 +49,12 @@
#include "common/settings.h"
#include "common/string_util.h"
#include "frontend_common/play_time_manager.h"
#include "core/constants.h"
#include "core/core.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.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/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/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
@@ -215,109 +210,6 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
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);
if (!loader) {
return;
@@ -334,6 +226,17 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
m_manual_provider->AddEntry(FileSys::TitleType::Application,
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
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());
}
}
}
}
@@ -1428,8 +1331,7 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
Common::Android::ToJString(env, patch.name),
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.title_id)),
static_cast<jlong>(patch.numeric_version));
Common::Android::ToJString(env, std::to_string(patch.title_id)));
env->SetObjectArrayElement(jpatchArray, i, jpatch);
++i;
}
@@ -1693,6 +1595,7 @@ JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_updatePowerState(
g_has_battery.store(hasBattery, std::memory_order_relaxed);
}
// return #ifdef ENABLE_UPDATE_CHECKER
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isUpdateCheckerEnabled(
JNIEnv* env,
jobject obj) {
@@ -1703,39 +1606,22 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isUpdateChecker
#endif
}
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_isNightlyBuild(
JNIEnv* env,
jobject obj) {
#ifdef NIGHTLY_BUILD
return JNI_TRUE;
#else
return JNI_FALSE;
#endif
}
#ifdef ENABLE_UPDATE_CHECKER
JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate(
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_checkForUpdate(
JNIEnv* env,
jobject obj) {
std::optional<UpdateChecker::Update> release = UpdateChecker::GetUpdate();
if (!release) return nullptr;
const bool is_prerelease = ((strstr(Common::g_build_version, "pre-alpha") != nullptr) ||
(strstr(Common::g_build_version, "alpha") != nullptr) ||
(strstr(Common::g_build_version, "beta") != nullptr) ||
(strstr(Common::g_build_version, "rc") != nullptr));
const std::optional<std::string> latest_release_tag =
UpdateChecker::GetLatestRelease(is_prerelease);
const std::string tag = release->tag;
const std::string name = release->name;
jobjectArray result = env->NewObjectArray(2, env->FindClass("java/lang/String"), nullptr);
const jstring jtag = env->NewStringUTF(tag.c_str());
const jstring jname = env->NewStringUTF(name.c_str());
env->SetObjectArrayElement(result, 0, jtag);
env->SetObjectArrayElement(result, 1, jname);
env->DeleteLocalRef(jtag);
env->DeleteLocalRef(jname);
return result;
if (latest_release_tag && latest_release_tag.value() != Common::g_build_version) {
return env->NewStringUTF(latest_release_tag.value().c_str());
}
return nullptr;
}
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateUrl(
@@ -1754,11 +1640,9 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateUrl(
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
JNIEnv* env,
jobject obj,
jstring tag,
jstring artifact,
jstring version,
jstring packageId) {
const char* version_str = env->GetStringUTFChars(tag, nullptr);
const char* artifact_str = env->GetStringUTFChars(artifact, nullptr);
const char* version_str = env->GetStringUTFChars(version, nullptr);
const char* package_id_str = env->GetStringUTFChars(packageId, nullptr);
std::string variant;
@@ -1769,22 +1653,17 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
} else if (package_id.find("com.miHoYo.Yuanshen") != std::string::npos) {
variant = "optimized";
} else {
#ifdef ARCHITECTURE_arm64
variant = "standard";
#else
variant = "chromeos";
#endif
}
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant);
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", version_str, variant);
const std::string url = fmt::format("{}/{}/releases/download/{}/{}",
std::string{Common::g_build_auto_update_website},
std::string{Common::g_build_auto_update_repo},
version_str,
apk_filename);
env->ReleaseStringUTFChars(tag, version_str);
env->ReleaseStringUTFChars(artifact, artifact_str);
env->ReleaseStringUTFChars(version, version_str);
env->ReleaseStringUTFChars(packageId, package_id_str);
return env->NewStringUTF(url.c_str());
}
@@ -1796,388 +1675,4 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion(
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"
+2 -26
View File
@@ -1,19 +1,18 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <string>
#include <jni.h>
#include <common/fs/path_util.h>
#include "android_config.h"
#include "android_settings.h"
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "frontend_common/config.h"
#include "frontend_common/settings_generator.h"
#include "native.h"
std::unique_ptr<AndroidConfig> global_config;
@@ -38,7 +37,6 @@ extern "C" {
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
global_config = std::make_unique<AndroidConfig>();
FrontendCommon::GenerateSettings();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
@@ -583,26 +581,4 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setSdmcDir(JNIEnv* env, jobject
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"
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/**
@@ -258,11 +258,6 @@ Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
}
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;
}
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -21,7 +18,7 @@
#include "input_common/drivers/virtual_gamepad.h"
#include "native.h"
ankerl::unordered_dense::map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
bool IsHandheldOnly() {
const auto npad_style_set =
@@ -1,9 +0,0 @@
<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>
@@ -1,9 +0,0 @@
<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>
@@ -220,23 +220,6 @@
android:textColor="?attr/colorOnPrimary"
app:backgroundTint="?attr/colorPrimary"
app:iconTint="?attr/colorOnPrimary"
app:rippleColor="#99FFFFFF"
/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/launch_qlaunch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="Launch QLaunch"
android:text="@string/qlaunch_applet"
app:icon="@drawable/ic_home"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:textColor="?attr/colorOnPrimary"
app:backgroundTint="?attr/colorPrimary"
app:iconTint="?attr/colorOnPrimary"
app:rippleColor="#99FFFFFF"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -11,7 +11,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:orientation="horizontal"
android:padding="16dp"
android:layout_gravity="center_vertical">
@@ -23,25 +23,12 @@
android:layout_gravity="center_vertical|start"
android:requiresFadingEdge="horizontal"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@+id/type_indicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
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
android:id="@+id/button_layout"
android:layout_width="wrap_content"
@@ -1,39 +0,0 @@
<?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>
@@ -1,226 +0,0 @@
<?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>
@@ -214,22 +214,6 @@
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/launch_qlaunch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="Launch QLaunch"
android:text="@string/qlaunch_applet"
app:icon="@drawable/ic_home"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:textColor="?attr/colorOnPrimary"
app:backgroundTint="?attr/colorPrimary"
app:iconTint="?attr/colorOnPrimary"
app:rippleColor="#99FFFFFF"
/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/add_directory"
android:layout_width="wrap_content"
@@ -243,7 +227,6 @@
android:textColor="?attr/colorOnPrimary"
app:backgroundTint="?attr/colorPrimary"
app:iconTint="?attr/colorOnPrimary"
app:rippleColor="#99FFFFFF"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,49 +0,0 @@
<?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>
@@ -1,21 +0,0 @@
<?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>
@@ -1,125 +0,0 @@
<?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
android:id="@+id/gamesFragment"
android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
android:label="PlatformGamesFragment">
android:label="PlatformGamesFragment" />
<action
android:id="@+id/action_gamesFragment_to_homeSettingsFragment"
app:destination="@id/homeSettingsFragment"
@@ -16,14 +16,6 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_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
android:id="@+id/homeSettingsFragment"
@@ -44,9 +36,6 @@
<action
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
app:destination="@id/gameFoldersFragment" />
<action
android:id="@+id/action_homeSettingsFragment_to_profileManagerFragment"
app:destination="@id/profileManagerFragment" />
</fragment>
<fragment
@@ -166,9 +155,6 @@
<action
android:id="@+id/action_global_perGamePropertiesFragment"
app:destination="@id/perGamePropertiesFragment" />
<action
android:id="@+id/action_global_gameInfoFragment"
app:destination="@id/gameInfoFragment" />
<fragment
android:id="@+id/gameInfoFragment"
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
@@ -200,17 +186,5 @@
app:nullable="true"
android:defaultValue="@null" />
</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>
@@ -94,6 +94,9 @@
<string name="use_sync_core">مزامنة سرعة النواة</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_description">يعمل هذا التحسين على تسريع وصول الذاكرة بواسطة البرنامج الضيف. يؤدي تمكينه إلى إجراء عمليات قراءة/كتابة ذاكرة الضيف مباشرة في الذاكرة والاستفادة من MMU المضيف. يؤدي تعطيل هذا إلى إجبار جميع عمليات الوصول إلى الذاكرة على استخدام محاكاة MMU البرمجية.</string>
<string name="debug_knobs">مقابض تصحيح الأخطاء</string>
@@ -58,6 +58,9 @@
<string name="use_sync_core">مزامنة سرعة النواة</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_description">ئەم باشکردنە خێرایی دەستکەوتنی بیرگە لەلایەن پرۆگرامی میوانەکە زیاد دەکات. چالاککردنی وای لێدەکات کە خوێندنەوە/نووسینەکانی بیرگەی میوانەکە ڕاستەوخۆ لە بیرگە ئەنجام بدرێت و میمیکردنی MMU میواندە بەکاربهێنێت. ناچالاککردنی ئەمە هەموو دەستکەوتنەکانی بیرگە ڕەت دەکاتەوە لە بەکارهێنانی میمیکردنی MMU نەرمەکاڵا.</string>
<!-- NVDEC Emulation -->
@@ -90,6 +90,9 @@
<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_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_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>
@@ -90,6 +90,9 @@
<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_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_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>
@@ -229,10 +232,6 @@
<string name="update_install_failed">Installieren des Updates fehlgeschlagen: %1$s</string>
<string name="home_search">Suche</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="manage_game_folders">Spiele-Ordner verwalten</string>
<string name="select_games_folder_description">Erlaubt Eden die Spieleliste zu füllen</string>
@@ -709,11 +708,6 @@ Wird der Handheld-Modus verwendet, verringert es die Auflösung und erhöht die
<string name="path_set">Pfad erfolgreich gesetzt</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 -->
<string name="info">Info</string>
<string name="info_description">Programm-ID, Entwickler, Version</string>
@@ -75,6 +75,9 @@
<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_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_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 -->
@@ -60,6 +60,8 @@
<string name="sync_memory_operations_description">اطمینان از سازگاری داده‌ها بین عملیات محاسباتی و حافظه. این گزینه ممکن است مشکلات برخی بازی‌ها را رفع کند، اما در برخی موارد ممکن است عملکرد را کاهش دهد. به نظر می‌رسد بازی‌های با Unreal Engine 4 بیشترین تأثیر را داشته باشند.</string>
<string name="buffer_reorder_disable">غیرفعال کردن مرتب‌سازی مجدد بافر</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_description">تعداد قابلیت‌هایی که می‌توان در حالت Extended Dynamic State استفاده کرد را کنترل می‌کند. اعداد بالاتر قابلیت‌های بیشتری را فعال کرده و می‌توانند عملکرد را افزایش دهند، اما ممکن است با برخی درایورها و فروشندگان مشکلاتی ایجاد کنند. مقدار پیش‌فرض بسته به سیستم و قابلیت‌های سخت‌افزاری شما ممکن است متفاوت باشد. این مقدار را می‌توان تغییر داد تا زمانی که پایداری و کیفیت بصری بهتری حاصل شود.</string>
<string name="disabled">غیرفعال</string>
@@ -75,6 +75,9 @@
<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_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 lutilisation 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_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>
@@ -62,6 +62,9 @@
<string name="use_sync_core">סנכרון מהירות ליבה</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_description">אופטימיזציה זו מאיצה את גישת הזיכרון על ידי התוכנית האורחת. הפעלתה גורמת לכך שפעולות קריאה/כתיבה לזיכרון האורח מתבצעות ישירות לזיכרון ומשתמשות ב-MMU של המארח. השבתת זאת מאלצת את כל גישות הזיכרון להשתמש באמולציית MMU תוכנתית.</string>
<string name="debug_knobs_description">לשימוש בפיתוח בלבד.</string>

Some files were not shown because too many files have changed in this diff Show More