Compare commits
197 Commits
android-19
...
android-21
Author | SHA1 | Date | |
---|---|---|---|
28a23c4380 | |||
c768c19898 | |||
7273701bc2 | |||
3bf0492edb | |||
ce2eb6e8ee | |||
8b47465586 | |||
3065ab0fd8 | |||
a2407a2964 | |||
16b79df836 | |||
6a4b25699d | |||
bd8635e26a | |||
4349cdba07 | |||
f2fb761bac | |||
59aee2b461 | |||
4d206d849e | |||
744c0173d1 | |||
55482ab5dc | |||
e56b44dee6 | |||
f2012e5aff | |||
4526fdaf64 | |||
bc22b4e782 | |||
f70821ce0d | |||
a774ff935c | |||
431df5ae93 | |||
677c2c2cd2 | |||
ee540c712c | |||
d23c4393fd | |||
b24a111136 | |||
91636deaaf | |||
68cbf67f4c | |||
645961613f | |||
53b321c945 | |||
975deb7528 | |||
2c049ae06d | |||
54372fdff5 | |||
c60ab6bbf6 | |||
cc09c265e1 | |||
0f9288e38d | |||
06fb7f90da | |||
e04368ad7c | |||
3e2d3548f2 | |||
eb9036d75b | |||
01a2d978eb | |||
6e67b25af9 | |||
e91667ba75 | |||
d45561ace0 | |||
0fdd6e8934 | |||
35794f4f18 | |||
b8be8dff69 | |||
bc317a9807 | |||
97ca160b08 | |||
1a3fc3724a | |||
7b01454d5f | |||
f3749394ac | |||
807f421752 | |||
e4915fb7d2 | |||
a76f6a2775 | |||
ba518f6899 | |||
ad4622da2c | |||
3b1c2896d9 | |||
fc5d76e6e2 | |||
5f9a45ada9 | |||
a120f8ff4d | |||
96833cd809 | |||
8649a80071 | |||
550cadbee4 | |||
8bd10473d6 | |||
8d708b0c79 | |||
beaab10c8f | |||
889c5d2705 | |||
17b0aac809 | |||
399220ddbc | |||
59080a3d1d | |||
3a25a217e6 | |||
961b5586a5 | |||
57ff934f0d | |||
92ce9273ee | |||
dd36d43ea1 | |||
a7a7720752 | |||
c725f3c86c | |||
1b984738ab | |||
a3199401f4 | |||
a7620a29be | |||
5ac1297fa5 | |||
fe69105f71 | |||
93a3342841 | |||
7b3e26acc9 | |||
444e86d191 | |||
61ce0088ae | |||
b3aa3633c7 | |||
627ba271ad | |||
2faa631676 | |||
5838779162 | |||
23fd1041c1 | |||
5c398ede6f | |||
378e4752a6 | |||
dad48f16b7 | |||
a363fa78ef | |||
03fa91ba3c | |||
d79d4d5986 | |||
ccd3dd842f | |||
b4a8e1ef8a | |||
5ea8f05ec6 | |||
10535e0016 | |||
a8c552e261 | |||
932bd98824 | |||
9f376cd901 | |||
a560b9f5a2 | |||
4f04bd3697 | |||
97c8b49444 | |||
748465f5a5 | |||
04867e2456 | |||
32f623e029 | |||
b6c6534c30 | |||
beb438bb0b | |||
4b963ca8a5 | |||
648ed55fe6 | |||
23430e6772 | |||
0672847330 | |||
a874ab0133 | |||
590d9b7e1d | |||
b0bca0f8b0 | |||
d8f1ce2f76 | |||
9b11b9dce5 | |||
303cd31162 | |||
0adc09e0af | |||
96fd1348ae | |||
bad705f245 | |||
34a8d0cc8e | |||
0a2536a0df | |||
c85d7ccd79 | |||
7a9d1ad2f8 | |||
2f0418c101 | |||
3092855d5a | |||
72f803c366 | |||
c87b96435d | |||
e4bbb24dcf | |||
6536d29c61 | |||
116f76e4b6 | |||
ce89580749 | |||
dff0a7c52a | |||
915efa4236 | |||
4548e5ae1d | |||
46c2435235 | |||
e9eb017aac | |||
0b0e9ef18d | |||
7f5adf8982 | |||
89d6856090 | |||
2cacb9d48c | |||
2c29c2b8dd | |||
16abda59be | |||
90ab89a0b0 | |||
6531ad56a6 | |||
e8671ed04e | |||
2044ae6b3a | |||
c661b95864 | |||
c683ec2bcb | |||
2e4e33156e | |||
04f4eeaca2 | |||
2e4b32204c | |||
34db13486a | |||
c6c6bb4041 | |||
a2ffb419c9 | |||
0127cec371 | |||
db3a6075f5 | |||
8876a15227 | |||
954eb40237 | |||
d4acdac168 | |||
817c7c445d | |||
da714a362b | |||
7b3941e5d4 | |||
15d8a40529 | |||
cdeaca73c4 | |||
bee22540a1 | |||
76880b84f9 | |||
2f0b57ca13 | |||
f90a022d3a | |||
f2fed21c11 | |||
d940974789 | |||
f7a3c135e2 | |||
fcb0dff67c | |||
b5dac5f525 | |||
2a0d707ce1 | |||
aae9eea532 | |||
2044a289f8 | |||
d3ba6b334b | |||
dac8c4ce4d | |||
9e974d4c7e | |||
6bfc3c530c | |||
93239f191a | |||
b17db2b462 | |||
9130366a58 | |||
ad0066a6b6 | |||
78c323c4eb | |||
51ad2d10de | |||
63b835f822 | |||
200b371d13 |
@ -32,3 +32,6 @@ if [ ! -z "$DIFF" ]; then
|
||||
echo "$DIFF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd src/android
|
||||
./gradlew ktlintCheck
|
||||
|
@ -8,17 +8,7 @@ variables:
|
||||
DisplayVersion: $[counter(variables['DisplayPrefix'], 1)]
|
||||
|
||||
stages:
|
||||
- stage: format
|
||||
displayName: 'format'
|
||||
jobs:
|
||||
- job: format
|
||||
displayName: 'clang'
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- template: ./templates/format-check.yml
|
||||
- stage: build
|
||||
dependsOn: format
|
||||
displayName: 'build'
|
||||
jobs:
|
||||
- job: build
|
||||
@ -43,7 +33,6 @@ stages:
|
||||
cache: 'true'
|
||||
version: $(DisplayVersion)
|
||||
- stage: build_win
|
||||
dependsOn: format
|
||||
displayName: 'build-windows'
|
||||
jobs:
|
||||
- job: build
|
||||
|
8
.github/workflows/verify.yml
vendored
8
.github/workflows/verify.yml
vendored
@ -13,13 +13,15 @@ jobs:
|
||||
format:
|
||||
name: 'verify format'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: yuzuemu/build-environments:linux-clang-format
|
||||
options: -u 1001
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: false
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: 'Verify Formatting'
|
||||
run: bash -ex ./.ci/scripts/format/script.sh
|
||||
build:
|
||||
|
@ -155,3 +155,7 @@ License: MIT
|
||||
Files: externals/gamemode/*
|
||||
Copyright: Copyright 2017-2019 Feral Interactive
|
||||
License: BSD-3-Clause
|
||||
|
||||
Files: src/android/app/debug.keystore
|
||||
Copyright: 2023 yuzu Emulator Project
|
||||
License: GPL-3.0-or-later
|
||||
|
@ -2,18 +2,20 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
find_path(SimpleIni_INCLUDE_DIR SimpleIni.h)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(SimpleIni
|
||||
REQUIRED_VARS SimpleIni_INCLUDE_DIR
|
||||
)
|
||||
|
||||
if (SimpleIni_FOUND AND NOT TARGET SimpleIni::SimpleIni)
|
||||
add_library(SimpleIni::SimpleIni INTERFACE IMPORTED)
|
||||
set_target_properties(SimpleIni::SimpleIni PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${SimpleIni_INCLUDE_DIR}"
|
||||
find_package(SimpleIni QUIET CONFIG)
|
||||
if (SimpleIni_CONSIDERED_CONFIGS)
|
||||
find_package_handle_standard_args(SimpleIni CONFIG_MODE)
|
||||
else()
|
||||
find_package(PkgConfig QUIET)
|
||||
pkg_search_module(SIMPLEINI QUIET IMPORTED_TARGET simpleini)
|
||||
find_package_handle_standard_args(SimpleIni
|
||||
REQUIRED_VARS SIMPLEINI_INCLUDEDIR
|
||||
VERSION_VAR SIMPLEINI_VERSION
|
||||
)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(SimpleIni_INCLUDE_DIR)
|
||||
if (SimpleIni_FOUND AND NOT TARGET SimpleIni::SimpleIni)
|
||||
add_library(SimpleIni::SimpleIni ALIAS PkgConfig::SIMPLEINI)
|
||||
endif()
|
||||
|
11
README.md
11
README.md
@ -1,3 +1,14 @@
|
||||
| Pull Request | Commit | Title | Author | Merged? |
|
||||
|----|----|----|----|----|
|
||||
| [12749](https://github.com/yuzu-emu/yuzu-android//pull/12749) | [`e3171486d`](https://github.com/yuzu-emu/yuzu-android//pull/12749/files) | general: workarounds for SMMU syncing issues | [liamwhite](https://github.com/liamwhite/) | Yes |
|
||||
| [12760](https://github.com/yuzu-emu/yuzu-android//pull/12760) | [`2c33ba278`](https://github.com/yuzu-emu/yuzu-android//pull/12760/files) | am: rewrite for multiprocess support | [liamwhite](https://github.com/liamwhite/) | Yes |
|
||||
| [12802](https://github.com/yuzu-emu/yuzu-android//pull/12802) | [`c5e88c654`](https://github.com/yuzu-emu/yuzu-android//pull/12802/files) | service: mii: Migrate service to new interface | [german77](https://github.com/german77/) | Yes |
|
||||
|
||||
|
||||
End of merge log. You can find the original README.md below the break.
|
||||
|
||||
-----
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
3
externals/CMakeLists.txt
vendored
3
externals/CMakeLists.txt
vendored
@ -178,6 +178,9 @@ if (NOT TARGET stb::headers)
|
||||
add_library(stb::headers ALIAS stb)
|
||||
endif()
|
||||
|
||||
add_library(tz tz/tz/tz.cpp)
|
||||
target_include_directories(tz PUBLIC ./tz)
|
||||
|
||||
add_library(bc_decoder bc_decoder/bc_decoder.cpp)
|
||||
target_include_directories(bc_decoder PUBLIC ./bc_decoder)
|
||||
|
||||
|
2
externals/nx_tzdb/CMakeLists.txt
vendored
2
externals/nx_tzdb/CMakeLists.txt
vendored
@ -32,7 +32,7 @@ set(NX_TZDB_ARCHIVE "${CMAKE_CURRENT_BINARY_DIR}/${NX_TZDB_VERSION}.zip")
|
||||
|
||||
set(NX_TZDB_ROMFS_DIR "${CMAKE_CURRENT_BINARY_DIR}/nx_tzdb")
|
||||
|
||||
if ((NOT CAN_BUILD_NX_TZDB OR YUZU_DOWNLOAD_TIME_ZONE_DATA) AND NOT EXISTS ${NX_TZDB_ARCHIVE})
|
||||
if ((NOT CAN_BUILD_NX_TZDB OR YUZU_DOWNLOAD_TIME_ZONE_DATA) AND NOT EXISTS ${NX_TZDB_ROMFS_DIR})
|
||||
set(NX_TZDB_DOWNLOAD_URL "https://github.com/lat9nq/tzdb_to_nx/releases/download/${NX_TZDB_VERSION}/${NX_TZDB_VERSION}.zip")
|
||||
|
||||
message(STATUS "Downloading time zone data from ${NX_TZDB_DOWNLOAD_URL}...")
|
||||
|
4
externals/nx_tzdb/NxTzdbCreateHeader.cmake
vendored
4
externals/nx_tzdb/NxTzdbCreateHeader.cmake
vendored
@ -11,6 +11,10 @@ execute_process(
|
||||
WORKING_DIRECTORY ${ZONE_PATH}
|
||||
OUTPUT_VARIABLE FILE_LIST)
|
||||
|
||||
if (NOT FILE_LIST)
|
||||
message(FATAL_ERROR "No timezone files found in directory ${ZONE_PATH}, did the download fail?")
|
||||
endif()
|
||||
|
||||
set(DIRECTORY_NAME ${HEADER_NAME})
|
||||
|
||||
set(FILE_DATA "")
|
||||
|
1636
externals/tz/tz/tz.cpp
vendored
Normal file
1636
externals/tz/tz/tz.cpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
81
externals/tz/tz/tz.h
vendored
Normal file
81
externals/tz/tz/tz.h
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: 1996 Arthur David Olson
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <array>
|
||||
#include <time.h>
|
||||
|
||||
namespace Tz {
|
||||
using u8 = uint8_t;
|
||||
using s8 = int8_t;
|
||||
using u16 = uint16_t;
|
||||
using s16 = int16_t;
|
||||
using u32 = uint32_t;
|
||||
using s32 = int32_t;
|
||||
using u64 = uint64_t;
|
||||
using s64 = int64_t;
|
||||
|
||||
constexpr size_t TZ_MAX_TIMES = 1000;
|
||||
constexpr size_t TZ_MAX_TYPES = 128;
|
||||
constexpr size_t TZ_MAX_CHARS = 50;
|
||||
constexpr size_t MY_TZNAME_MAX = 255;
|
||||
constexpr size_t TZNAME_MAXIMUM = 255;
|
||||
constexpr size_t TZ_MAX_LEAPS = 50;
|
||||
constexpr s64 TIME_T_MAX = std::numeric_limits<s64>::max();
|
||||
constexpr s64 TIME_T_MIN = std::numeric_limits<s64>::min();
|
||||
constexpr size_t CHARS_EXTRA = 3;
|
||||
constexpr size_t MAX_ZONE_CHARS = std::max(TZ_MAX_CHARS + CHARS_EXTRA, sizeof("UTC"));
|
||||
constexpr size_t MAX_TZNAME_CHARS = 2 * (MY_TZNAME_MAX + 1);
|
||||
|
||||
struct ttinfo {
|
||||
s32 tt_utoff;
|
||||
bool tt_isdst;
|
||||
s32 tt_desigidx;
|
||||
bool tt_ttisstd;
|
||||
bool tt_ttisut;
|
||||
};
|
||||
static_assert(sizeof(ttinfo) == 0x10, "ttinfo has the wrong size!");
|
||||
|
||||
struct Rule {
|
||||
s32 timecnt;
|
||||
s32 typecnt;
|
||||
s32 charcnt;
|
||||
bool goback;
|
||||
bool goahead;
|
||||
std::array <u8, 0x2> padding0;
|
||||
std::array<s64, TZ_MAX_TIMES> ats;
|
||||
std::array<u8, TZ_MAX_TIMES> types;
|
||||
std::array<ttinfo, TZ_MAX_TYPES> ttis;
|
||||
std::array<char, std::max(MAX_ZONE_CHARS, MAX_TZNAME_CHARS)> chars;
|
||||
s32 defaulttype;
|
||||
std::array <u8, 0x12C4> padding1;
|
||||
};
|
||||
static_assert(sizeof(Rule) == 0x4000, "Rule has the wrong size!");
|
||||
|
||||
struct CalendarTimeInternal {
|
||||
s32 tm_sec;
|
||||
s32 tm_min;
|
||||
s32 tm_hour;
|
||||
s32 tm_mday;
|
||||
s32 tm_mon;
|
||||
s32 tm_year;
|
||||
s32 tm_wday;
|
||||
s32 tm_yday;
|
||||
s32 tm_isdst;
|
||||
std::array<char, 16> tm_zone;
|
||||
s32 tm_utoff;
|
||||
s32 time_index;
|
||||
};
|
||||
static_assert(sizeof(CalendarTimeInternal) == 0x3C, "CalendarTimeInternal has the wrong size!");
|
||||
|
||||
s32 ParseTimeZoneBinary(Rule& out_rule, std::span<const u8> binary);
|
||||
|
||||
bool localtime_rz(CalendarTimeInternal* tmp, Rule* sp, time_t* timep);
|
||||
u32 mktime_tzname(time_t* out_time, Rule* sp, CalendarTimeInternal* tmp);
|
||||
|
||||
} // namespace Tz
|
@ -82,8 +82,8 @@ android {
|
||||
}
|
||||
|
||||
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
|
||||
if (keystoreFile != null) {
|
||||
signingConfigs {
|
||||
signingConfigs {
|
||||
if (keystoreFile != null) {
|
||||
create("release") {
|
||||
storeFile = file(keystoreFile)
|
||||
storePassword = System.getenv("ANDROID_KEYSTORE_PASS")
|
||||
@ -91,6 +91,12 @@ android {
|
||||
keyPassword = System.getenv("ANDROID_KEYSTORE_PASS")
|
||||
}
|
||||
}
|
||||
create("default") {
|
||||
storeFile = file("$projectDir/debug.keystore")
|
||||
storePassword = "android"
|
||||
keyAlias = "androiddebugkey"
|
||||
keyPassword = "android"
|
||||
}
|
||||
}
|
||||
|
||||
// Define build types, which are orthogonal to product flavors.
|
||||
@ -101,7 +107,7 @@ android {
|
||||
signingConfig = if (keystoreFile != null) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
signingConfigs.getByName("default")
|
||||
}
|
||||
|
||||
resValue("string", "app_name_suffixed", "yuzu")
|
||||
@ -118,7 +124,7 @@ android {
|
||||
register("relWithDebInfo") {
|
||||
isDefault = true
|
||||
resValue("string", "app_name_suffixed", "yuzu Debug Release")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("default")
|
||||
isMinifyEnabled = true
|
||||
isDebuggable = true
|
||||
proguardFiles(
|
||||
@ -133,6 +139,7 @@ android {
|
||||
// Signed by debug key disallowing distribution on Play Store.
|
||||
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
|
||||
debug {
|
||||
signingConfig = signingConfigs.getByName("default")
|
||||
resValue("string", "app_name_suffixed", "yuzu Debug")
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
@ -188,8 +195,15 @@ tasks.create<Delete>("ktlintReset") {
|
||||
delete(File(buildDir.path + File.separator + "intermediates/ktLint"))
|
||||
}
|
||||
|
||||
val showFormatHelp = {
|
||||
logger.lifecycle(
|
||||
"If this check fails, please try running \"gradlew ktlintFormat\" for automatic " +
|
||||
"codestyle fixes"
|
||||
)
|
||||
}
|
||||
tasks.getByPath("ktlintKotlinScriptCheck").doFirst { showFormatHelp.invoke() }
|
||||
tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
|
||||
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
|
||||
tasks.getByPath("preBuild").dependsOn("ktlintCheck")
|
||||
|
||||
ktlint {
|
||||
version.set("0.47.1")
|
||||
@ -228,71 +242,33 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||
}
|
||||
|
||||
fun getGitVersion(): String {
|
||||
var versionName = "0.0"
|
||||
|
||||
try {
|
||||
versionName = ProcessBuilder("git", "describe", "--always", "--long")
|
||||
fun runGitCommand(command: List<String>): String {
|
||||
return try {
|
||||
ProcessBuilder(command)
|
||||
.directory(project.rootDir)
|
||||
.redirectOutput(ProcessBuilder.Redirect.PIPE)
|
||||
.redirectError(ProcessBuilder.Redirect.PIPE)
|
||||
.start().inputStream.bufferedReader().use { it.readText() }
|
||||
.trim()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Cannot find git")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun getGitVersion(): String {
|
||||
val versionName = if (System.getenv("GITHUB_ACTIONS") != null) {
|
||||
val gitTag = System.getenv("GIT_TAG_NAME") ?: ""
|
||||
gitTag
|
||||
} else {
|
||||
runGitCommand(listOf("git", "describe", "--always", "--long"))
|
||||
.replace(Regex("(-0)?-[^-]+$"), "")
|
||||
} catch (e: Exception) {
|
||||
logger.error("Cannot find git, defaulting to dummy version number")
|
||||
}
|
||||
|
||||
if (System.getenv("GITHUB_ACTIONS") != null) {
|
||||
val gitTag = System.getenv("GIT_TAG_NAME")
|
||||
versionName = gitTag ?: versionName
|
||||
}
|
||||
|
||||
return versionName
|
||||
return versionName.ifEmpty { "0.0" }
|
||||
}
|
||||
|
||||
fun getGitHash(): String {
|
||||
try {
|
||||
val processBuilder = ProcessBuilder("git", "rev-parse", "--short", "HEAD")
|
||||
processBuilder.directory(project.rootDir)
|
||||
val process = processBuilder.start()
|
||||
val inputStream = process.inputStream
|
||||
val errorStream = process.errorStream
|
||||
process.waitFor()
|
||||
fun getGitHash(): String =
|
||||
runGitCommand(listOf("git", "rev-parse", "--short", "HEAD")).ifEmpty { "dummy-hash" }
|
||||
|
||||
return if (process.exitValue() == 0) {
|
||||
inputStream.bufferedReader()
|
||||
.use { it.readText().trim() } // return the value of gitHash
|
||||
} else {
|
||||
val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
|
||||
logger.error("Error running git command: $errorMessage")
|
||||
"dummy-hash" // return a dummy hash value in case of an error
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$e: Cannot find git, defaulting to dummy build hash")
|
||||
return "dummy-hash" // return a dummy hash value in case of an error
|
||||
}
|
||||
}
|
||||
|
||||
fun getBranch(): String {
|
||||
try {
|
||||
val processBuilder = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
processBuilder.directory(project.rootDir)
|
||||
val process = processBuilder.start()
|
||||
val inputStream = process.inputStream
|
||||
val errorStream = process.errorStream
|
||||
process.waitFor()
|
||||
|
||||
return if (process.exitValue() == 0) {
|
||||
inputStream.bufferedReader()
|
||||
.use { it.readText().trim() } // return the value of gitHash
|
||||
} else {
|
||||
val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
|
||||
logger.error("Error running git command: $errorMessage")
|
||||
"dummy-hash" // return a dummy hash value in case of an error
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$e: Cannot find git, defaulting to dummy build hash")
|
||||
return "dummy-hash" // return a dummy hash value in case of an error
|
||||
}
|
||||
}
|
||||
fun getBranch(): String =
|
||||
runGitCommand(listOf("git", "rev-parse", "--abbrev-ref", "HEAD")).ifEmpty { "dummy-hash" }
|
||||
|
BIN
src/android/app/debug.keystore
Normal file
BIN
src/android/app/debug.keystore
Normal file
Binary file not shown.
@ -21,6 +21,9 @@ import org.yuzu.yuzu_emu.utils.DocumentsTree
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
|
||||
import org.yuzu.yuzu_emu.model.InstallResult
|
||||
import org.yuzu.yuzu_emu.model.Patch
|
||||
import org.yuzu.yuzu_emu.model.GameVerificationResult
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
@ -235,9 +238,12 @@ object NativeLibrary {
|
||||
/**
|
||||
* Installs a nsp or xci file to nand
|
||||
* @param filename String representation of file uri
|
||||
* @param extension Lowercase string representation of file extension without "."
|
||||
* @return int representation of [InstallResult]
|
||||
*/
|
||||
external fun installFileToNand(filename: String, extension: String): Int
|
||||
external fun installFileToNand(
|
||||
filename: String,
|
||||
callback: (max: Long, progress: Long) -> Boolean
|
||||
): Int
|
||||
|
||||
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
|
||||
|
||||
@ -297,6 +303,11 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun getCpuBackend(): String
|
||||
|
||||
/**
|
||||
* Returns the current GPU Driver.
|
||||
*/
|
||||
external fun getGpuDriver(): String
|
||||
|
||||
external fun applySettings()
|
||||
|
||||
external fun logSettings()
|
||||
@ -535,9 +546,49 @@ object NativeLibrary {
|
||||
*
|
||||
* @param path Path to game file. Can be a [Uri].
|
||||
* @param programId String representation of a game's program ID
|
||||
* @return Array of pairs where the first value is the name of an addon and the second is the version
|
||||
* @return Array of available patches
|
||||
*/
|
||||
external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
|
||||
external fun getPatchesForFile(path: String, programId: String): Array<Patch>?
|
||||
|
||||
/**
|
||||
* Removes an update for a given [programId]
|
||||
* @param programId String representation of a game's program ID
|
||||
*/
|
||||
external fun removeUpdate(programId: String)
|
||||
|
||||
/**
|
||||
* Removes all DLC for a [programId]
|
||||
* @param programId String representation of a game's program ID
|
||||
*/
|
||||
external fun removeDLC(programId: String)
|
||||
|
||||
/**
|
||||
* Removes a mod installed for a given [programId]
|
||||
* @param programId String representation of a game's program ID
|
||||
* @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name
|
||||
* of the mod's directory in a game's load folder.
|
||||
*/
|
||||
external fun removeMod(programId: String, name: String)
|
||||
|
||||
/**
|
||||
* Verifies all installed content
|
||||
* @param callback UI callback for verification progress. Return true in the callback to cancel.
|
||||
* @return Array of content that failed verification. Successful if empty.
|
||||
*/
|
||||
external fun verifyInstalledContents(
|
||||
callback: (max: Long, progress: Long) -> Boolean
|
||||
): Array<String>
|
||||
|
||||
/**
|
||||
* Verifies the contents of a game
|
||||
* @param path String path to a game
|
||||
* @param callback UI callback for verification progress. Return true in the callback to cancel.
|
||||
* @return Int that is meant to be converted to a [GameVerificationResult]
|
||||
*/
|
||||
external fun verifyGameContents(
|
||||
path: String,
|
||||
callback: (max: Long, progress: Long) -> Boolean
|
||||
): Int
|
||||
|
||||
/**
|
||||
* Gets the save location for a specific game
|
||||
@ -568,6 +619,11 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun clearFilesystemProvider()
|
||||
|
||||
/**
|
||||
* Checks if all necessary keys are present for decryption
|
||||
*/
|
||||
external fun areKeysPresent(): Boolean
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
@ -609,15 +665,4 @@ object NativeLibrary {
|
||||
const val RELEASED = 0
|
||||
const val PRESSED = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from installFileToNand
|
||||
*/
|
||||
object InstallFileToNandResult {
|
||||
const val Success = 0
|
||||
const val SuccessFileOverwritten = 1
|
||||
const val Error = 2
|
||||
const val ErrorBaseGame = 3
|
||||
const val ErrorFilenameExtension = 4
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,6 @@ import org.yuzu.yuzu_emu.utils.ForegroundService
|
||||
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
import org.yuzu.yuzu_emu.utils.MemoryUtil
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.NfcReader
|
||||
import org.yuzu.yuzu_emu.utils.ThemeHelper
|
||||
import java.text.NumberFormat
|
||||
@ -171,11 +170,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
stopMotionSensorListener()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
NativeConfig.saveGlobalConfig()
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
|
||||
@ -199,6 +193,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
if (emulationViewModel.drawerOpen.value) {
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
return InputHandler.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
@ -209,6 +207,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||
return super.dispatchGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
if (emulationViewModel.drawerOpen.value) {
|
||||
return super.dispatchGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
// Don't attempt to do anything if we are disconnecting a device.
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
return true
|
||||
|
@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate
|
||||
* code used in every [RecyclerView].
|
||||
* Type assigned to [Model] must inherit from [Object] in order to be compared properly.
|
||||
* @param exact Decides whether each item will be compared by reference or by their contents
|
||||
*/
|
||||
abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
|
||||
exact: Boolean = true
|
||||
) : ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>(exact)).build()) {
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
private class DiffCallback<Model>(val exact: Boolean) : DiffUtil.ItemCallback<Model>() {
|
||||
override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
|
||||
if (exact) {
|
||||
return oldItem === newItem
|
||||
}
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
/**
|
||||
* Generic list class meant to take care of basic lists
|
||||
* @param currentList The list to show initially
|
||||
*/
|
||||
abstract class AbstractListAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
|
||||
open var currentList: List<Model>
|
||||
) : RecyclerView.Adapter<Holder>() {
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
/**
|
||||
* Adds an item to [currentList] and notifies the underlying adapter of the change. If no parameter
|
||||
* is passed in for position, [item] is added to the end of the list. Invokes [callback] last.
|
||||
* @param item The item to add to the list
|
||||
* @param position Index where [item] will be added
|
||||
* @param callback Lambda that's called at the end of the list changes and has the added list
|
||||
* position passed in as a parameter
|
||||
*/
|
||||
open fun addItem(item: Model, position: Int = -1, callback: ((position: Int) -> Unit)? = null) {
|
||||
val newList = currentList.toMutableList()
|
||||
val positionToUpdate: Int
|
||||
if (position == -1) {
|
||||
newList.add(item)
|
||||
currentList = newList
|
||||
positionToUpdate = currentList.size - 1
|
||||
} else {
|
||||
newList.add(position, item)
|
||||
currentList = newList
|
||||
positionToUpdate = position
|
||||
}
|
||||
onItemAdded(positionToUpdate, callback)
|
||||
}
|
||||
|
||||
protected fun onItemAdded(position: Int, callback: ((Int) -> Unit)? = null) {
|
||||
notifyItemInserted(position)
|
||||
callback?.invoke(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the [item] at [position] in the [currentList] and notifies the underlying adapter
|
||||
* of the change. Invokes [callback] last.
|
||||
* @param item New list item
|
||||
* @param position Index where [item] will replace the existing list item
|
||||
* @param callback Lambda that's called at the end of the list changes and has the changed list
|
||||
* position passed in as a parameter
|
||||
*/
|
||||
fun changeItem(item: Model, position: Int, callback: ((position: Int) -> Unit)? = null) {
|
||||
val newList = currentList.toMutableList()
|
||||
newList[position] = item
|
||||
currentList = newList
|
||||
onItemChanged(position, callback)
|
||||
}
|
||||
|
||||
protected fun onItemChanged(position: Int, callback: ((Int) -> Unit)? = null) {
|
||||
notifyItemChanged(position)
|
||||
callback?.invoke(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the list item at [position] in [currentList] and notifies the underlying adapter
|
||||
* of the change. Invokes [callback] last.
|
||||
* @param position Index where the list item will be removed
|
||||
* @param callback Lambda that's called at the end of the list changes and has the removed list
|
||||
* position passed in as a parameter
|
||||
*/
|
||||
fun removeItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
|
||||
val newList = currentList.toMutableList()
|
||||
newList.removeAt(position)
|
||||
currentList = newList
|
||||
onItemRemoved(position, callback)
|
||||
}
|
||||
|
||||
protected fun onItemRemoved(position: Int, callback: ((Int) -> Unit)? = null) {
|
||||
notifyItemRemoved(position)
|
||||
callback?.invoke(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces [currentList] with [newList] and notifies the underlying adapter of the change.
|
||||
* @param newList The new list to replace [currentList]
|
||||
*/
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
open fun replaceList(newList: List<Model>) {
|
||||
currentList = newList
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import org.yuzu.yuzu_emu.model.SelectableItem
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
/**
|
||||
* Generic list class meant to take care of single selection UI updates
|
||||
* @param currentList The list to show initially
|
||||
* @param defaultSelection The default selection to use if no list items are selected by
|
||||
* [SelectableItem.selected] or if the currently selected item is removed from the list
|
||||
*/
|
||||
abstract class AbstractSingleSelectionList<
|
||||
Model : SelectableItem,
|
||||
Holder : AbstractViewHolder<Model>
|
||||
>(
|
||||
final override var currentList: List<Model>,
|
||||
private val defaultSelection: DefaultSelection = DefaultSelection.Start
|
||||
) : AbstractListAdapter<Model, Holder>(currentList) {
|
||||
var selectedItem = getDefaultSelection()
|
||||
|
||||
init {
|
||||
findSelectedItem()
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the selection state of the [SelectableItem] that was selected and the previously selected
|
||||
* item and notifies the underlying adapter of the change for those items. Invokes [callback] last.
|
||||
* Does nothing if [position] is the same as the currently selected item.
|
||||
* @param position Index of the item that was selected
|
||||
* @param callback Lambda that's called at the end of the list changes and has the selected list
|
||||
* position passed in as a parameter
|
||||
*/
|
||||
fun selectItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
|
||||
if (position == selectedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
val previouslySelectedItem = selectedItem
|
||||
selectedItem = position
|
||||
if (currentList.indices.contains(selectedItem)) {
|
||||
currentList[selectedItem].onSelectionStateChanged(true)
|
||||
}
|
||||
if (currentList.indices.contains(previouslySelectedItem)) {
|
||||
currentList[previouslySelectedItem].onSelectionStateChanged(false)
|
||||
}
|
||||
onItemChanged(previouslySelectedItem)
|
||||
onItemChanged(selectedItem)
|
||||
callback?.invoke(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given item from the list and notifies the underlying adapter of the change. If the
|
||||
* currently selected item was the item that was removed, the item at the position provided
|
||||
* by [defaultSelection] will be made the new selection. Invokes [callback] last.
|
||||
* @param position Index of the item that was removed
|
||||
* @param callback Lambda that's called at the end of the list changes and has the removed and
|
||||
* selected list positions passed in as parameters
|
||||
*/
|
||||
fun removeSelectableItem(
|
||||
position: Int,
|
||||
callback: ((removedPosition: Int, selectedPosition: Int) -> Unit)?
|
||||
) {
|
||||
removeItem(position)
|
||||
if (position == selectedItem) {
|
||||
selectedItem = getDefaultSelection()
|
||||
currentList[selectedItem].onSelectionStateChanged(true)
|
||||
onItemChanged(selectedItem)
|
||||
} else if (position < selectedItem) {
|
||||
selectedItem--
|
||||
}
|
||||
callback?.invoke(position, selectedItem)
|
||||
}
|
||||
|
||||
override fun addItem(item: Model, position: Int, callback: ((Int) -> Unit)?) {
|
||||
super.addItem(item, position, callback)
|
||||
if (position <= selectedItem && position != -1) {
|
||||
selectedItem++
|
||||
}
|
||||
}
|
||||
|
||||
override fun replaceList(newList: List<Model>) {
|
||||
super.replaceList(newList)
|
||||
findSelectedItem()
|
||||
}
|
||||
|
||||
private fun findSelectedItem() {
|
||||
for (i in currentList.indices) {
|
||||
if (currentList[i].selected) {
|
||||
selectedItem = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDefaultSelection(): Int =
|
||||
when (defaultSelection) {
|
||||
DefaultSelection.Start -> currentList.indices.first
|
||||
DefaultSelection.End -> currentList.indices.last
|
||||
}
|
||||
|
||||
enum class DefaultSelection { Start, End }
|
||||
}
|
@ -5,48 +5,33 @@ package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
|
||||
import org.yuzu.yuzu_emu.model.Addon
|
||||
import org.yuzu.yuzu_emu.model.Patch
|
||||
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>(
|
||||
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||
) {
|
||||
class AddonAdapter(val addonViewModel: AddonViewModel) :
|
||||
AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
|
||||
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return AddonViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
override fun onBindViewHolder(holder: AddonViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(addon: Addon) {
|
||||
AbstractViewHolder<Patch>(binding) {
|
||||
override fun bind(model: Patch) {
|
||||
binding.root.setOnClickListener {
|
||||
binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
|
||||
binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked
|
||||
}
|
||||
binding.title.text = addon.title
|
||||
binding.version.text = addon.version
|
||||
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
||||
addon.enabled = checked
|
||||
binding.title.text = model.name
|
||||
binding.version.text = model.version
|
||||
binding.addonCheckbox.setOnCheckedChangeListener { _, checked ->
|
||||
model.enabled = checked
|
||||
}
|
||||
binding.addonCheckbox.isChecked = model.enabled
|
||||
binding.buttonDelete.setOnClickListener {
|
||||
addonViewModel.setAddonToDelete(model)
|
||||
}
|
||||
binding.addonSwitch.isChecked = addon.enabled
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
|
||||
override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,11 @@
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
@ -19,72 +17,58 @@ import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
|
||||
import org.yuzu.yuzu_emu.model.Applet
|
||||
import org.yuzu.yuzu_emu.model.AppletInfo
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
|
||||
RecyclerView.Adapter<AppletAdapter.AppletViewHolder>(),
|
||||
View.OnClickListener {
|
||||
|
||||
class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) :
|
||||
AbstractListAdapter<Applet, AppletAdapter.AppletViewHolder>(applets) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): AppletAdapter.AppletViewHolder {
|
||||
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.apply { root.setOnClickListener(this@AppletAdapter) }
|
||||
.also { return AppletViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AppletViewHolder, position: Int) =
|
||||
holder.bind(applets[position])
|
||||
|
||||
override fun getItemCount(): Int = applets.size
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val applet = (view.tag as AppletViewHolder).applet
|
||||
val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
|
||||
if (appletPath.isEmpty()) {
|
||||
Toast.makeText(
|
||||
YuzuApplication.appContext,
|
||||
R.string.applets_error_applet,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (applet.appletInfo == AppletInfo.Cabinet) {
|
||||
view.findNavController()
|
||||
.navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
|
||||
return
|
||||
}
|
||||
|
||||
NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
|
||||
val appletGame = Game(
|
||||
title = YuzuApplication.appContext.getString(applet.titleId),
|
||||
path = appletPath
|
||||
)
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
|
||||
view.findNavController().navigate(action)
|
||||
}
|
||||
|
||||
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var applet: Applet
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(applet: Applet) {
|
||||
this.applet = applet
|
||||
|
||||
binding.title.setText(applet.titleId)
|
||||
binding.description.setText(applet.descriptionId)
|
||||
AbstractViewHolder<Applet>(binding) {
|
||||
override fun bind(model: Applet) {
|
||||
binding.title.setText(model.titleId)
|
||||
binding.description.setText(model.descriptionId)
|
||||
binding.icon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
binding.icon.context.resources,
|
||||
applet.iconId,
|
||||
model.iconId,
|
||||
binding.icon.context.theme
|
||||
)
|
||||
)
|
||||
|
||||
binding.root.setOnClickListener { onClick(model) }
|
||||
}
|
||||
|
||||
fun onClick(applet: Applet) {
|
||||
val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
|
||||
if (appletPath.isEmpty()) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
R.string.applets_error_applet,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (applet.appletInfo == AppletInfo.Cabinet) {
|
||||
binding.root.findNavController()
|
||||
.navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
|
||||
return
|
||||
}
|
||||
|
||||
NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
|
||||
val appletGame = Game(
|
||||
title = YuzuApplication.appContext.getString(applet.titleId),
|
||||
path = appletPath
|
||||
)
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
|
||||
binding.root.findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,10 @@
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
@ -19,54 +17,43 @@ import org.yuzu.yuzu_emu.model.CabinetMode
|
||||
import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder
|
||||
import org.yuzu.yuzu_emu.model.AppletInfo
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class CabinetLauncherDialogAdapter(val fragment: Fragment) :
|
||||
RecyclerView.Adapter<CabinetModeViewHolder>(),
|
||||
View.OnClickListener {
|
||||
private val cabinetModes = CabinetMode.values().copyOfRange(1, CabinetMode.values().size)
|
||||
AbstractListAdapter<CabinetMode, CabinetModeViewHolder>(
|
||||
CabinetMode.values().copyOfRange(1, CabinetMode.entries.size).toList()
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder {
|
||||
DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.apply { root.setOnClickListener(this@CabinetLauncherDialogAdapter) }
|
||||
.also { return CabinetModeViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = cabinetModes.size
|
||||
|
||||
override fun onBindViewHolder(holder: CabinetModeViewHolder, position: Int) =
|
||||
holder.bind(cabinetModes[position])
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val mode = (view.tag as CabinetModeViewHolder).cabinetMode
|
||||
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
|
||||
NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
|
||||
NativeLibrary.setCabinetMode(mode.id)
|
||||
val appletGame = Game(
|
||||
title = YuzuApplication.appContext.getString(R.string.cabinet_applet),
|
||||
path = appletPath
|
||||
)
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
|
||||
fragment.findNavController().navigate(action)
|
||||
}
|
||||
|
||||
inner class CabinetModeViewHolder(val binding: DialogListItemBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var cabinetMode: CabinetMode
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(cabinetMode: CabinetMode) {
|
||||
this.cabinetMode = cabinetMode
|
||||
AbstractViewHolder<CabinetMode>(binding) {
|
||||
override fun bind(model: CabinetMode) {
|
||||
binding.icon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
binding.icon.context.resources,
|
||||
cabinetMode.iconId,
|
||||
model.iconId,
|
||||
binding.icon.context.theme
|
||||
)
|
||||
)
|
||||
binding.title.setText(cabinetMode.titleId)
|
||||
binding.title.setText(model.titleId)
|
||||
|
||||
binding.root.setOnClickListener { onClick(model) }
|
||||
}
|
||||
|
||||
private fun onClick(mode: CabinetMode) {
|
||||
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
|
||||
NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
|
||||
NativeLibrary.setCabinetMode(mode.id)
|
||||
val appletGame = Game(
|
||||
title = YuzuApplication.appContext.getString(R.string.cabinet_applet),
|
||||
path = appletPath
|
||||
)
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
|
||||
fragment.findNavController().navigate(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,65 +7,39 @@ import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||
import org.yuzu.yuzu_emu.model.Driver
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class DriverAdapter(private val driverViewModel: DriverViewModel) :
|
||||
ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
|
||||
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||
AbstractSingleSelectionList<Driver, DriverAdapter.DriverViewHolder>(
|
||||
driverViewModel.driverList.value
|
||||
) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
|
||||
val binding =
|
||||
CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return DriverViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
private fun onSelectDriver(position: Int) {
|
||||
driverViewModel.setSelectedDriverIndex(position)
|
||||
notifyItemChanged(driverViewModel.previouslySelectedDriver)
|
||||
notifyItemChanged(driverViewModel.selectedDriver)
|
||||
}
|
||||
|
||||
private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) {
|
||||
if (driverViewModel.selectedDriver > position) {
|
||||
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
|
||||
}
|
||||
if (GpuDriverHelper.customDriverSettingData == driverData.second) {
|
||||
driverViewModel.setSelectedDriverIndex(0)
|
||||
}
|
||||
driverViewModel.driversToDelete.add(driverData.first)
|
||||
driverViewModel.removeDriver(driverData)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemChanged(driverViewModel.selectedDriver)
|
||||
CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return DriverViewHolder(it) }
|
||||
}
|
||||
|
||||
inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private lateinit var driverData: Pair<String, GpuDriverMetadata>
|
||||
|
||||
fun bind(driverData: Pair<String, GpuDriverMetadata>) {
|
||||
this.driverData = driverData
|
||||
val driver = driverData.second
|
||||
|
||||
AbstractViewHolder<Driver>(binding) {
|
||||
override fun bind(model: Driver) {
|
||||
binding.apply {
|
||||
radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
|
||||
radioButton.isChecked = model.selected
|
||||
root.setOnClickListener {
|
||||
onSelectDriver(bindingAdapterPosition)
|
||||
selectItem(bindingAdapterPosition) {
|
||||
driverViewModel.onDriverSelected(it)
|
||||
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
|
||||
}
|
||||
}
|
||||
buttonDelete.setOnClickListener {
|
||||
onDeleteDriver(driverData, bindingAdapterPosition)
|
||||
removeSelectableItem(
|
||||
bindingAdapterPosition
|
||||
) { removedPosition: Int, selectedPosition: Int ->
|
||||
driverViewModel.onDriverRemoved(removedPosition, selectedPosition)
|
||||
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
|
||||
}
|
||||
}
|
||||
|
||||
// Delay marquee by 3s
|
||||
@ -80,38 +54,19 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
|
||||
},
|
||||
3000
|
||||
)
|
||||
if (driver.name == null) {
|
||||
title.setText(R.string.system_gpu_driver)
|
||||
description.text = ""
|
||||
version.text = ""
|
||||
version.visibility = View.GONE
|
||||
description.visibility = View.GONE
|
||||
buttonDelete.visibility = View.GONE
|
||||
} else {
|
||||
title.text = driver.name
|
||||
version.text = driver.version
|
||||
description.text = driver.description
|
||||
title.text = model.title
|
||||
version.text = model.version
|
||||
description.text = model.description
|
||||
if (model.description.isNotEmpty()) {
|
||||
version.visibility = View.VISIBLE
|
||||
description.visibility = View.VISIBLE
|
||||
buttonDelete.visibility = View.VISIBLE
|
||||
} else {
|
||||
version.visibility = View.GONE
|
||||
description.visibility = View.GONE
|
||||
buttonDelete.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Pair<String, GpuDriverMetadata>,
|
||||
newItem: Pair<String, GpuDriverMetadata>
|
||||
): Boolean {
|
||||
return oldItem.first == newItem.first
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Pair<String, GpuDriverMetadata>,
|
||||
newItem: Pair<String, GpuDriverMetadata>
|
||||
): Boolean {
|
||||
return oldItem.second == newItem.second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,19 +8,14 @@ import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
||||
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
|
||||
ListAdapter<GameDir, FolderAdapter.FolderViewHolder>(
|
||||
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||
) {
|
||||
AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
@ -29,18 +24,11 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
|
||||
.also { return FolderViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
inner class FolderViewHolder(val binding: CardFolderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private lateinit var gameDir: GameDir
|
||||
|
||||
fun bind(gameDir: GameDir) {
|
||||
this.gameDir = gameDir
|
||||
|
||||
AbstractViewHolder<GameDir>(binding) {
|
||||
override fun bind(model: GameDir) {
|
||||
binding.apply {
|
||||
path.text = Uri.parse(gameDir.uriString).path
|
||||
path.text = Uri.parse(model.uriString).path
|
||||
path.postDelayed(
|
||||
{
|
||||
path.isSelected = true
|
||||
@ -50,7 +38,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
|
||||
)
|
||||
|
||||
buttonEdit.setOnClickListener {
|
||||
GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir)
|
||||
GameFolderPropertiesDialogFragment.newInstance(model)
|
||||
.show(
|
||||
activity.supportFragmentManager,
|
||||
GameFolderPropertiesDialogFragment.TAG
|
||||
@ -58,19 +46,9 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
|
||||
}
|
||||
|
||||
buttonDelete.setOnClickListener {
|
||||
gamesViewModel.removeFolder(this@FolderViewHolder.gameDir)
|
||||
gamesViewModel.removeFolder(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
|
||||
override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,155 +3,46 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||
import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
|
||||
import org.yuzu.yuzu_emu.databinding.CardGameBinding
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||
// Create a new view.
|
||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.cardGame.setOnClickListener(this)
|
||||
binding.cardGame.setOnLongClickListener(this)
|
||||
|
||||
// Use that view to create a ViewHolder.
|
||||
return GameViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
/**
|
||||
* Launches the game that was clicked on.
|
||||
*
|
||||
* @param view The card representing the game the user wants to play.
|
||||
*/
|
||||
override fun onClick(view: View) {
|
||||
val holder = view.tag as GameViewHolder
|
||||
|
||||
val gameExists = DocumentFile.fromSingleUri(
|
||||
YuzuApplication.appContext,
|
||||
Uri.parse(holder.game.path)
|
||||
)?.exists() == true
|
||||
if (!gameExists) {
|
||||
Toast.makeText(
|
||||
YuzuApplication.appContext,
|
||||
R.string.loader_error_file_not_found,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
return
|
||||
}
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
preferences.edit()
|
||||
.putLong(
|
||||
holder.game.keyLastPlayedTime,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
.apply()
|
||||
|
||||
val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse(holder.game.path)
|
||||
}
|
||||
|
||||
activity.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val layerDrawable = ResourcesCompat.getDrawable(
|
||||
YuzuApplication.appContext.resources,
|
||||
R.drawable.shortcut,
|
||||
null
|
||||
) as LayerDrawable
|
||||
layerDrawable.setDrawableByLayerId(
|
||||
R.id.shortcut_foreground,
|
||||
GameIconUtils.getGameIcon(activity, holder.game)
|
||||
.toDrawable(YuzuApplication.appContext.resources)
|
||||
)
|
||||
val inset = YuzuApplication.appContext.resources
|
||||
.getDimensionPixelSize(R.dimen.icon_inset)
|
||||
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
|
||||
val shortcut =
|
||||
ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path)
|
||||
.setShortLabel(holder.game.title)
|
||||
.setIcon(
|
||||
IconCompat.createWithAdaptiveBitmap(
|
||||
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
|
||||
)
|
||||
)
|
||||
.setIntent(openIntent)
|
||||
.build()
|
||||
ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
|
||||
view.findNavController().navigate(action)
|
||||
}
|
||||
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
val holder = view.tag as GameViewHolder
|
||||
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
|
||||
view.findNavController().navigate(action)
|
||||
return true
|
||||
CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return GameViewHolder(it) }
|
||||
}
|
||||
|
||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var game: Game
|
||||
|
||||
init {
|
||||
binding.cardGame.tag = this
|
||||
}
|
||||
|
||||
fun bind(game: Game) {
|
||||
this.game = game
|
||||
|
||||
AbstractViewHolder<Game>(binding) {
|
||||
override fun bind(model: Game) {
|
||||
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
GameIconUtils.loadGameIcon(game, binding.imageGameScreen)
|
||||
GameIconUtils.loadGameIcon(model, binding.imageGameScreen)
|
||||
|
||||
binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
|
||||
binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
|
||||
|
||||
binding.textGameTitle.postDelayed(
|
||||
{
|
||||
@ -160,16 +51,56 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||
},
|
||||
3000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
return oldItem == newItem
|
||||
binding.cardGame.setOnClickListener { onClick(model) }
|
||||
binding.cardGame.setOnLongClickListener { onLongClick(model) }
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
return oldItem == newItem
|
||||
fun onClick(game: Game) {
|
||||
val gameExists = DocumentFile.fromSingleUri(
|
||||
YuzuApplication.appContext,
|
||||
Uri.parse(game.path)
|
||||
)?.exists() == true
|
||||
if (!gameExists) {
|
||||
Toast.makeText(
|
||||
YuzuApplication.appContext,
|
||||
R.string.loader_error_file_not_found,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
return
|
||||
}
|
||||
|
||||
val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
preferences.edit()
|
||||
.putLong(
|
||||
game.keyLastPlayedTime,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
.apply()
|
||||
|
||||
activity.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val shortcut =
|
||||
ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path)
|
||||
.setShortLabel(game.title)
|
||||
.setIcon(GameIconUtils.getShortcutIcon(activity, game))
|
||||
.setIntent(game.launchIntent)
|
||||
.build()
|
||||
ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
|
||||
}
|
||||
}
|
||||
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true)
|
||||
binding.root.findNavController().navigate(action)
|
||||
}
|
||||
|
||||
fun onLongClick(game: Game): Boolean {
|
||||
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
|
||||
binding.root.findNavController().navigate(action)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,23 +12,22 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
|
||||
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
|
||||
import org.yuzu.yuzu_emu.model.GameProperty
|
||||
import org.yuzu.yuzu_emu.model.InstallableProperty
|
||||
import org.yuzu.yuzu_emu.model.SubmenuProperty
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class GamePropertiesAdapter(
|
||||
private val viewLifecycle: LifecycleOwner,
|
||||
private var properties: List<GameProperty>
|
||||
) :
|
||||
RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
|
||||
) : AbstractListAdapter<GameProperty, AbstractViewHolder<GameProperty>>(properties) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): GamePropertyViewHolder {
|
||||
): AbstractViewHolder<GameProperty> {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
PropertyType.Submenu.ordinal -> {
|
||||
@ -51,11 +50,6 @@ class GamePropertiesAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = properties.size
|
||||
|
||||
override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) =
|
||||
holder.bind(properties[position])
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (properties[position]) {
|
||||
is SubmenuProperty -> PropertyType.Submenu.ordinal
|
||||
@ -63,14 +57,10 @@ class GamePropertiesAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
abstract fun bind(property: GameProperty)
|
||||
}
|
||||
|
||||
inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
|
||||
GamePropertyViewHolder(binding.root) {
|
||||
override fun bind(property: GameProperty) {
|
||||
val submenuProperty = property as SubmenuProperty
|
||||
AbstractViewHolder<GameProperty>(binding) {
|
||||
override fun bind(model: GameProperty) {
|
||||
val submenuProperty = model as SubmenuProperty
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
submenuProperty.action.invoke()
|
||||
@ -108,9 +98,9 @@ class GamePropertiesAdapter(
|
||||
}
|
||||
|
||||
inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
|
||||
GamePropertyViewHolder(binding.root) {
|
||||
override fun bind(property: GameProperty) {
|
||||
val installableProperty = property as InstallableProperty
|
||||
AbstractViewHolder<GameProperty>(binding) {
|
||||
override fun bind(model: GameProperty) {
|
||||
val installableProperty = model as InstallableProperty
|
||||
|
||||
binding.title.setText(installableProperty.titleId)
|
||||
binding.description.setText(installableProperty.descriptionId)
|
||||
|
@ -14,69 +14,37 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
|
||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.HomeSetting
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class HomeSettingAdapter(
|
||||
private val activity: AppCompatActivity,
|
||||
private val viewLifecycle: LifecycleOwner,
|
||||
var options: List<HomeSetting>
|
||||
) :
|
||||
RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(),
|
||||
View.OnClickListener {
|
||||
options: List<HomeSetting>
|
||||
) : AbstractListAdapter<HomeSetting, HomeSettingAdapter.HomeOptionViewHolder>(options) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
|
||||
val binding =
|
||||
CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.root.setOnClickListener(this)
|
||||
return HomeOptionViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return options.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
|
||||
holder.bind(options[position])
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val holder = view.tag as HomeOptionViewHolder
|
||||
if (holder.option.isEnabled.invoke()) {
|
||||
holder.option.onClick.invoke()
|
||||
} else {
|
||||
MessageDialogFragment.newInstance(
|
||||
activity,
|
||||
titleId = holder.option.disabledTitleId,
|
||||
descriptionId = holder.option.disabledMessageId
|
||||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return HomeOptionViewHolder(it) }
|
||||
}
|
||||
|
||||
inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var option: HomeSetting
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(option: HomeSetting) {
|
||||
this.option = option
|
||||
binding.optionTitle.text = activity.resources.getString(option.titleId)
|
||||
binding.optionDescription.text = activity.resources.getString(option.descriptionId)
|
||||
AbstractViewHolder<HomeSetting>(binding) {
|
||||
override fun bind(model: HomeSetting) {
|
||||
binding.optionTitle.text = activity.resources.getString(model.titleId)
|
||||
binding.optionDescription.text = activity.resources.getString(model.descriptionId)
|
||||
binding.optionIcon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
option.iconId,
|
||||
model.iconId,
|
||||
activity.theme
|
||||
)
|
||||
)
|
||||
|
||||
when (option.titleId) {
|
||||
when (model.titleId) {
|
||||
R.string.get_early_access ->
|
||||
binding.optionLayout.background =
|
||||
ContextCompat.getDrawable(
|
||||
@ -85,7 +53,7 @@ class HomeSettingAdapter(
|
||||
)
|
||||
}
|
||||
|
||||
if (!option.isEnabled.invoke()) {
|
||||
if (!model.isEnabled.invoke()) {
|
||||
binding.optionTitle.alpha = 0.5f
|
||||
binding.optionDescription.alpha = 0.5f
|
||||
binding.optionIcon.alpha = 0.5f
|
||||
@ -93,7 +61,7 @@ class HomeSettingAdapter(
|
||||
|
||||
viewLifecycle.lifecycleScope.launch {
|
||||
viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
option.details.collect { updateOptionDetails(it) }
|
||||
model.details.collect { updateOptionDetails(it) }
|
||||
}
|
||||
}
|
||||
binding.optionDetail.postDelayed(
|
||||
@ -103,6 +71,20 @@ class HomeSettingAdapter(
|
||||
},
|
||||
3000
|
||||
)
|
||||
|
||||
binding.root.setOnClickListener { onClick(model) }
|
||||
}
|
||||
|
||||
private fun onClick(model: HomeSetting) {
|
||||
if (model.isEnabled.invoke()) {
|
||||
model.onClick.invoke()
|
||||
} else {
|
||||
MessageDialogFragment.newInstance(
|
||||
activity,
|
||||
titleId = model.disabledTitleId,
|
||||
descriptionId = model.disabledMessageId
|
||||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOptionDetails(detailString: String) {
|
||||
|
@ -6,43 +6,33 @@ package org.yuzu.yuzu_emu.adapters
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
|
||||
import org.yuzu.yuzu_emu.model.Installable
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class InstallableAdapter(private val installables: List<Installable>) :
|
||||
RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() {
|
||||
class InstallableAdapter(installables: List<Installable>) :
|
||||
AbstractListAdapter<Installable, InstallableAdapter.InstallableViewHolder>(installables) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): InstallableAdapter.InstallableViewHolder {
|
||||
val binding =
|
||||
CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return InstallableViewHolder(binding)
|
||||
CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return InstallableViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = installables.size
|
||||
|
||||
override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) =
|
||||
holder.bind(installables[position])
|
||||
|
||||
inner class InstallableViewHolder(val binding: CardInstallableBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var installable: Installable
|
||||
AbstractViewHolder<Installable>(binding) {
|
||||
override fun bind(model: Installable) {
|
||||
binding.title.setText(model.titleId)
|
||||
binding.description.setText(model.descriptionId)
|
||||
|
||||
fun bind(installable: Installable) {
|
||||
this.installable = installable
|
||||
|
||||
binding.title.setText(installable.titleId)
|
||||
binding.description.setText(installable.descriptionId)
|
||||
|
||||
if (installable.install != null) {
|
||||
if (model.install != null) {
|
||||
binding.buttonInstall.visibility = View.VISIBLE
|
||||
binding.buttonInstall.setOnClickListener { installable.install.invoke() }
|
||||
binding.buttonInstall.setOnClickListener { model.install.invoke() }
|
||||
}
|
||||
if (installable.export != null) {
|
||||
if (model.export != null) {
|
||||
binding.buttonExport.visibility = View.VISIBLE
|
||||
binding.buttonExport.setOnClickListener { installable.export.invoke() }
|
||||
binding.buttonExport.setOnClickListener { model.export.invoke() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,49 +7,33 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
||||
import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.License
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) :
|
||||
RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(),
|
||||
View.OnClickListener {
|
||||
class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
|
||||
AbstractListAdapter<License, LicenseAdapter.LicenseViewHolder>(licenses) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
|
||||
val binding =
|
||||
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.root.setOnClickListener(this)
|
||||
return LicenseViewHolder(binding)
|
||||
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return LicenseViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = licenses.size
|
||||
inner class LicenseViewHolder(val binding: ListItemSettingBinding) :
|
||||
AbstractViewHolder<License>(binding) {
|
||||
override fun bind(model: License) {
|
||||
binding.apply {
|
||||
textSettingName.text = root.context.getString(model.titleId)
|
||||
textSettingDescription.text = root.context.getString(model.descriptionId)
|
||||
textSettingValue.visibility = View.GONE
|
||||
|
||||
override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) {
|
||||
holder.bind(licenses[position])
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val license = (view.tag as LicenseViewHolder).license
|
||||
LicenseBottomSheetDialogFragment.newInstance(license)
|
||||
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
|
||||
}
|
||||
|
||||
inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
|
||||
lateinit var license: License
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
root.setOnClickListener { onClick(model) }
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(license: License) {
|
||||
this.license = license
|
||||
|
||||
val context = YuzuApplication.appContext
|
||||
binding.textSettingName.text = context.getString(license.titleId)
|
||||
binding.textSettingDescription.text = context.getString(license.descriptionId)
|
||||
binding.textSettingValue.visibility = View.GONE
|
||||
private fun onClick(license: License) {
|
||||
LicenseBottomSheetDialogFragment.newInstance(license)
|
||||
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.yuzu.yuzu_emu.databinding.PageSetupBinding
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
@ -18,31 +17,19 @@ import org.yuzu.yuzu_emu.model.SetupCallback
|
||||
import org.yuzu.yuzu_emu.model.SetupPage
|
||||
import org.yuzu.yuzu_emu.model.StepState
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||
|
||||
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
|
||||
RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
|
||||
class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
|
||||
AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
|
||||
val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SetupPageViewHolder(binding)
|
||||
PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return SetupPageViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pages.size
|
||||
|
||||
override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
|
||||
holder.bind(pages[position])
|
||||
|
||||
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
|
||||
RecyclerView.ViewHolder(binding.root), SetupCallback {
|
||||
lateinit var page: SetupPage
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(page: SetupPage) {
|
||||
this.page = page
|
||||
|
||||
if (page.stepCompleted.invoke() == StepState.COMPLETE) {
|
||||
AbstractViewHolder<SetupPage>(binding), SetupCallback {
|
||||
override fun bind(model: SetupPage) {
|
||||
if (model.stepCompleted.invoke() == StepState.COMPLETE) {
|
||||
binding.buttonAction.visibility = View.INVISIBLE
|
||||
binding.textConfirmation.visibility = View.VISIBLE
|
||||
}
|
||||
@ -50,31 +37,31 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
|
||||
binding.icon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
page.iconId,
|
||||
model.iconId,
|
||||
activity.theme
|
||||
)
|
||||
)
|
||||
binding.textTitle.text = activity.resources.getString(page.titleId)
|
||||
binding.textTitle.text = activity.resources.getString(model.titleId)
|
||||
binding.textDescription.text =
|
||||
Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
|
||||
Html.fromHtml(activity.resources.getString(model.descriptionId), 0)
|
||||
|
||||
binding.buttonAction.apply {
|
||||
text = activity.resources.getString(page.buttonTextId)
|
||||
if (page.buttonIconId != 0) {
|
||||
text = activity.resources.getString(model.buttonTextId)
|
||||
if (model.buttonIconId != 0) {
|
||||
icon = ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
page.buttonIconId,
|
||||
model.buttonIconId,
|
||||
activity.theme
|
||||
)
|
||||
}
|
||||
iconGravity =
|
||||
if (page.leftAlignedIcon) {
|
||||
if (model.leftAlignedIcon) {
|
||||
MaterialButton.ICON_GRAVITY_START
|
||||
} else {
|
||||
MaterialButton.ICON_GRAVITY_END
|
||||
}
|
||||
setOnClickListener {
|
||||
page.buttonAction.invoke(this@SetupPageViewHolder)
|
||||
model.buttonAction.invoke(this@SetupPageViewHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,8 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
|
||||
THEME("theme"),
|
||||
THEME_MODE("theme_mode"),
|
||||
OVERLAY_SCALE("control_scale"),
|
||||
OVERLAY_OPACITY("control_opacity");
|
||||
OVERLAY_OPACITY("control_opacity"),
|
||||
LOCK_DRAWER("lock_drawer");
|
||||
|
||||
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
|
||||
|
||||
|
@ -76,8 +76,8 @@ class AboutFragment : Fragment() {
|
||||
binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
|
||||
}
|
||||
|
||||
binding.textBuildHash.text = BuildConfig.GIT_HASH
|
||||
binding.buttonBuildHash.setOnClickListener {
|
||||
binding.textVersionName.text = BuildConfig.VERSION_NAME
|
||||
binding.buttonVersionName.setOnClickListener {
|
||||
val clipBoard =
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(getString(R.string.build), BuildConfig.GIT_HASH)
|
||||
|
@ -74,7 +74,7 @@ class AddonsFragment : Fragment() {
|
||||
|
||||
binding.listAddons.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = AddonAdapter()
|
||||
adapter = AddonAdapter(addonViewModel)
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.apply {
|
||||
@ -110,6 +110,21 @@ class AddonsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
addonViewModel.addonToDelete.collect {
|
||||
if (it != null) {
|
||||
MessageDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
titleId = R.string.confirm_uninstall,
|
||||
descriptionId = R.string.confirm_uninstall_description,
|
||||
positiveAction = { addonViewModel.onDeleteAddon(it) }
|
||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||
addonViewModel.setAddonToDelete(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.buttonInstall.setOnClickListener {
|
||||
@ -156,22 +171,22 @@ class AddonsFragment : Fragment() {
|
||||
descriptionId = R.string.invalid_directory_description
|
||||
)
|
||||
if (isValid) {
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.installing_game_content,
|
||||
false
|
||||
) {
|
||||
) { progressCallback, _ ->
|
||||
val parentDirectoryName = externalAddonDirectory.name
|
||||
val internalAddonDirectory =
|
||||
File(args.game.addonDir + parentDirectoryName)
|
||||
try {
|
||||
externalAddonDirectory.copyFilesTo(internalAddonDirectory)
|
||||
externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback)
|
||||
} catch (_: Exception) {
|
||||
return@newInstance errorMessage
|
||||
}
|
||||
addonViewModel.refreshAddons()
|
||||
return@newInstance getString(R.string.addon_installed_successfully)
|
||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
} else {
|
||||
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@ -13,20 +14,26 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.DriverAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||
import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@ -55,12 +62,43 @@ class DriverManagerFragment : Fragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
// This is using the correct scope, lint is just acting up
|
||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
driverViewModel.onOpenDriverManager(args.game)
|
||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||
binding.toolbarDrivers.inflateMenu(R.menu.menu_driver_manager)
|
||||
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
|
||||
binding.toolbarDrivers.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_driver_use_global -> {
|
||||
StringSetting.DRIVER_PATH.global = true
|
||||
driverViewModel.updateDriverList()
|
||||
(binding.listDrivers.adapter as DriverAdapter)
|
||||
.replaceList(driverViewModel.driverList.value)
|
||||
driverViewModel.showClearButton(false)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.apply {
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
driverViewModel.showClearButton.collect {
|
||||
binding.toolbarDrivers.menu
|
||||
.findItem(R.id.menu_driver_use_global).isVisible = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!driverViewModel.isInteractionAllowed.value) {
|
||||
DriversLoadingDialogFragment().show(
|
||||
@ -85,25 +123,6 @@ class DriverManagerFragment : Fragment() {
|
||||
adapter = DriverAdapter(driverViewModel)
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.apply {
|
||||
launch {
|
||||
driverViewModel.driverList.collectLatest {
|
||||
(binding.listDrivers.adapter as DriverAdapter).submitList(it)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
driverViewModel.newDriverInstalled.collect {
|
||||
if (_binding != null && it) {
|
||||
(binding.listDrivers.adapter as DriverAdapter).apply {
|
||||
notifyItemChanged(driverViewModel.previouslySelectedDriver)
|
||||
notifyItemChanged(driverViewModel.selectedDriver)
|
||||
driverViewModel.setNewDriverInstalled(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInsets()
|
||||
}
|
||||
|
||||
@ -154,13 +173,13 @@ class DriverManagerFragment : Fragment() {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.installing_driver,
|
||||
false
|
||||
) {
|
||||
) { _, _ ->
|
||||
val driverPath =
|
||||
"${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}"
|
||||
"${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
|
||||
val driverFile = File(driverPath)
|
||||
|
||||
// Ignore file exceptions when a user selects an invalid zip
|
||||
@ -177,14 +196,23 @@ class DriverManagerFragment : Fragment() {
|
||||
|
||||
val driverData = GpuDriverHelper.getMetadataFromZip(driverFile)
|
||||
val driverInList =
|
||||
driverViewModel.driverList.value.firstOrNull { it.second == driverData }
|
||||
driverViewModel.driverData.firstOrNull { it.second == driverData }
|
||||
if (driverInList != null) {
|
||||
return@newInstance getString(R.string.driver_already_installed)
|
||||
} else {
|
||||
driverViewModel.addDriver(Pair(driverPath, driverData))
|
||||
driverViewModel.setNewDriverInstalled(true)
|
||||
driverViewModel.onDriverAdded(Pair(driverPath, driverData))
|
||||
withContext(Dispatchers.Main) {
|
||||
if (_binding != null) {
|
||||
val adapter = binding.listDrivers.adapter as DriverAdapter
|
||||
adapter.addItem(driverData.toDriver())
|
||||
adapter.selectItem(adapter.currentList.indices.last)
|
||||
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
|
||||
binding.listDrivers
|
||||
.smoothScrollToPosition(adapter.currentList.indices.last)
|
||||
}
|
||||
}
|
||||
}
|
||||
return@newInstance Any()
|
||||
}.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(childFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +141,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
||||
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
||||
retainInstance = true
|
||||
emulationState = EmulationState(game.path)
|
||||
emulationState = EmulationState(game.path) {
|
||||
return@EmulationState driverViewModel.isInteractionAllowed.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -182,11 +184,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
override fun onDrawerOpened(drawerView: View) {
|
||||
// No op
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
binding.inGameMenu.requestFocus()
|
||||
emulationViewModel.setDrawerOpen(true)
|
||||
}
|
||||
|
||||
override fun onDrawerClosed(drawerView: View) {
|
||||
// No op
|
||||
binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt())
|
||||
emulationViewModel.setDrawerOpen(false)
|
||||
}
|
||||
|
||||
override fun onDrawerStateChanged(newState: Int) {
|
||||
@ -196,6 +201,28 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
|
||||
game.title
|
||||
|
||||
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) {
|
||||
R.string.unlock_drawer
|
||||
} else {
|
||||
R.string.lock_drawer
|
||||
}
|
||||
val iconId = if (lockMode == DrawerLayout.LOCK_MODE_UNLOCKED) {
|
||||
R.drawable.ic_unlock
|
||||
} else {
|
||||
R.drawable.ic_lock
|
||||
}
|
||||
|
||||
title = getString(titleId)
|
||||
icon = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
iconId,
|
||||
requireContext().theme
|
||||
)
|
||||
}
|
||||
|
||||
binding.inGameMenu.setNavigationItemSelectedListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_pause_emulation -> {
|
||||
@ -216,6 +243,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
requireContext().theme
|
||||
)
|
||||
}
|
||||
binding.inGameMenu.requestFocus()
|
||||
true
|
||||
}
|
||||
|
||||
@ -224,6 +252,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
null,
|
||||
Settings.MenuTag.SECTION_ROOT
|
||||
)
|
||||
binding.inGameMenu.requestFocus()
|
||||
binding.root.findNavController().navigate(action)
|
||||
true
|
||||
}
|
||||
@ -233,6 +262,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
args.game,
|
||||
Settings.MenuTag.SECTION_ROOT
|
||||
)
|
||||
binding.inGameMenu.requestFocus()
|
||||
binding.root.findNavController().navigate(action)
|
||||
true
|
||||
}
|
||||
@ -242,11 +272,39 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_lock_drawer -> {
|
||||
when (IntSetting.LOCK_DRAWER.getInt()) {
|
||||
DrawerLayout.LOCK_MODE_UNLOCKED -> {
|
||||
IntSetting.LOCK_DRAWER.setInt(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
it.title = resources.getString(R.string.unlock_drawer)
|
||||
it.icon = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
R.drawable.ic_lock,
|
||||
requireContext().theme
|
||||
)
|
||||
}
|
||||
|
||||
DrawerLayout.LOCK_MODE_LOCKED_CLOSED -> {
|
||||
IntSetting.LOCK_DRAWER.setInt(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
it.title = resources.getString(R.string.lock_drawer)
|
||||
it.icon = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
R.drawable.ic_unlock,
|
||||
requireContext().theme
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.inGameMenu.requestFocus()
|
||||
NativeConfig.saveGlobalConfig()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_exit -> {
|
||||
emulationState.stop()
|
||||
NativeConfig.reloadGlobalConfig()
|
||||
emulationViewModel.setIsEmulationStopping(true)
|
||||
binding.drawerLayout.close()
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
binding.inGameMenu.requestFocus()
|
||||
true
|
||||
}
|
||||
|
||||
@ -263,12 +321,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
if (!NativeLibrary.isRunning()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.drawerLayout.isOpen) {
|
||||
binding.drawerLayout.close()
|
||||
} else {
|
||||
binding.drawerLayout.open()
|
||||
}
|
||||
emulationViewModel.setDrawerOpen(!binding.drawerLayout.isOpen)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -322,11 +375,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
driverViewModel.isInteractionAllowed.collect {
|
||||
if (it) {
|
||||
startEmulation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
emulationViewModel.emulationStarted.collectLatest {
|
||||
if (it) {
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt())
|
||||
ViewUtils.showView(binding.surfaceInputOverlay)
|
||||
ViewUtils.hideView(binding.loadingIndicator)
|
||||
|
||||
@ -351,10 +413,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
driverViewModel.isInteractionAllowed.collect {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
emulationViewModel.drawerOpen.collect {
|
||||
if (it) {
|
||||
onEmulationStart()
|
||||
binding.drawerLayout.open()
|
||||
binding.inGameMenu.requestFocus()
|
||||
} else {
|
||||
binding.drawerLayout.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -362,7 +427,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEmulationStart() {
|
||||
private fun startEmulation() {
|
||||
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
|
||||
if (!DirectoryInitialization.areDirectoriesReady) {
|
||||
DirectoryInitialization.start()
|
||||
@ -437,12 +502,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
val FRAMETIME = 2
|
||||
val SPEED = 3
|
||||
perfStatsUpdater = {
|
||||
if (emulationViewModel.emulationStarted.value) {
|
||||
if (emulationViewModel.emulationStarted.value &&
|
||||
!emulationViewModel.isEmulationStopping.value
|
||||
) {
|
||||
val perfStats = NativeLibrary.getPerfStats()
|
||||
val cpuBackend = NativeLibrary.getCpuBackend()
|
||||
val gpuDriver = NativeLibrary.getGpuDriver()
|
||||
if (_binding != null) {
|
||||
binding.showFpsText.text =
|
||||
String.format("FPS: %.1f\n%s", perfStats[FPS], cpuBackend)
|
||||
String.format("FPS: %.1f\n%s/%s", perfStats[FPS], cpuBackend, gpuDriver)
|
||||
}
|
||||
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800)
|
||||
}
|
||||
@ -554,6 +622,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean()
|
||||
}
|
||||
|
||||
popup.setOnDismissListener { NativeConfig.saveGlobalConfig() }
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_toggle_fps -> {
|
||||
@ -720,7 +789,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.emulation_control_adjust)
|
||||
.setView(adjustBinding.root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
NativeConfig.saveGlobalConfig()
|
||||
}
|
||||
.setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
|
||||
setControlScale(50)
|
||||
setControlOpacity(100)
|
||||
@ -756,7 +827,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private class EmulationState(private val gamePath: String) {
|
||||
private class EmulationState(
|
||||
private val gamePath: String,
|
||||
private val emulationCanStart: () -> Boolean
|
||||
) {
|
||||
private var state: State
|
||||
private var surface: Surface? = null
|
||||
|
||||
@ -850,6 +924,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
State.PAUSED -> Log.warning(
|
||||
"[EmulationFragment] Surface cleared while emulation paused."
|
||||
)
|
||||
|
||||
else -> Log.warning(
|
||||
"[EmulationFragment] Surface cleared while emulation stopped."
|
||||
)
|
||||
@ -859,6 +934,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
|
||||
private fun runWithValidSurface() {
|
||||
NativeLibrary.surfaceChanged(surface)
|
||||
if (!emulationCanStart.invoke()) {
|
||||
return
|
||||
}
|
||||
|
||||
when (state) {
|
||||
State.STOPPED -> {
|
||||
val emulationThread = Thread({
|
||||
|
@ -21,8 +21,10 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
|
||||
import org.yuzu.yuzu_emu.model.GameVerificationResult
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||
|
||||
@ -101,6 +103,38 @@ class GameInfoFragment : Fragment() {
|
||||
""".trimIndent()
|
||||
copyToClipboard(args.game.title, details)
|
||||
}
|
||||
|
||||
buttonVerifyIntegrity.setOnClickListener {
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.verifying,
|
||||
true
|
||||
) { progressCallback, _ ->
|
||||
val result = GameVerificationResult.from(
|
||||
NativeLibrary.verifyGameContents(
|
||||
args.game.path,
|
||||
progressCallback
|
||||
)
|
||||
)
|
||||
return@newInstance when (result) {
|
||||
GameVerificationResult.Success ->
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.verify_success,
|
||||
descriptionId = R.string.operation_completed_successfully
|
||||
)
|
||||
GameVerificationResult.Failed ->
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.verify_failure,
|
||||
descriptionId = R.string.verify_failure_description
|
||||
)
|
||||
GameVerificationResult.NotImplemented ->
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.verify_no_result,
|
||||
descriptionId = R.string.verify_no_result_description
|
||||
)
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
setInsets()
|
||||
|
@ -4,6 +4,8 @@
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
@ -44,7 +46,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.MemoryUtil
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
|
||||
@ -85,6 +86,24 @@ class GamePropertiesFragment : Fragment() {
|
||||
view.findNavController().popBackStack()
|
||||
}
|
||||
|
||||
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java)
|
||||
binding.buttonShortcut.isEnabled = shortcutManager.isRequestPinShortcutSupported
|
||||
binding.buttonShortcut.setOnClickListener {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val shortcut = ShortcutInfo.Builder(requireContext(), args.game.title)
|
||||
.setShortLabel(args.game.title)
|
||||
.setIcon(
|
||||
GameIconUtils.getShortcutIcon(requireActivity(), args.game)
|
||||
.toIcon(requireContext())
|
||||
)
|
||||
.setIntent(args.game.launchIntent)
|
||||
.build()
|
||||
shortcutManager.requestPinShortcut(shortcut, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
|
||||
binding.title.text = args.game.title
|
||||
binding.title.postDelayed(
|
||||
@ -357,27 +376,17 @@ class GamePropertiesFragment : Fragment() {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val inputZip = requireContext().contentResolver.openInputStream(result)
|
||||
val savesFolder = File(args.game.saveDir)
|
||||
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||
cacheSaveDir.mkdir()
|
||||
|
||||
if (inputZip == null) {
|
||||
Toast.makeText(
|
||||
YuzuApplication.appContext,
|
||||
getString(R.string.fatal_error),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.save_files_importing,
|
||||
false
|
||||
) {
|
||||
) { _, _ ->
|
||||
try {
|
||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
||||
FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir)
|
||||
val files = cacheSaveDir.listFiles()
|
||||
var savesFolderFile: File? = null
|
||||
if (files != null) {
|
||||
@ -422,7 +431,7 @@ class GamePropertiesFragment : Fragment() {
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -436,11 +445,11 @@ class GamePropertiesFragment : Fragment() {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.save_files_exporting,
|
||||
false
|
||||
) {
|
||||
) { _, _ ->
|
||||
val saveLocation = args.game.saveDir
|
||||
val zipResult = FileUtil.zipFromInternalStorage(
|
||||
File(saveLocation),
|
||||
@ -452,6 +461,6 @@ class GamePropertiesFragment : Fragment() {
|
||||
TaskState.Completed -> getString(R.string.export_success)
|
||||
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
||||
}
|
||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import org.yuzu.yuzu_emu.BuildConfig
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
|
||||
import org.yuzu.yuzu_emu.features.DocumentProvider
|
||||
@ -140,6 +141,44 @@ class HomeSettingsFragment : Fragment() {
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.verify_installed_content,
|
||||
R.string.verify_installed_content_description,
|
||||
R.drawable.ic_check_circle,
|
||||
{
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
titleId = R.string.verifying,
|
||||
cancellable = true
|
||||
) { progressCallback, _ ->
|
||||
val result = NativeLibrary.verifyInstalledContents(progressCallback)
|
||||
return@newInstance if (progressCallback.invoke(100, 100)) {
|
||||
// Invoke the progress callback to check if the process was cancelled
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.verify_no_result,
|
||||
descriptionId = R.string.verify_no_result_description
|
||||
)
|
||||
} else if (result.isEmpty()) {
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.verify_success,
|
||||
descriptionId = R.string.operation_completed_successfully
|
||||
)
|
||||
} else {
|
||||
val failedNames = result.joinToString("\n")
|
||||
val errorMessage = YuzuApplication.appContext.getString(
|
||||
R.string.verification_failed_for,
|
||||
failedNames
|
||||
)
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.verify_failure,
|
||||
descriptionString = errorMessage
|
||||
)
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.share_log,
|
||||
|
@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
@ -195,26 +194,20 @@ class InstallableFragment : Fragment() {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val inputZip = requireContext().contentResolver.openInputStream(result)
|
||||
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||
cacheSaveDir.mkdir()
|
||||
|
||||
if (inputZip == null) {
|
||||
Toast.makeText(
|
||||
YuzuApplication.appContext,
|
||||
getString(R.string.fatal_error),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.save_files_importing,
|
||||
false
|
||||
) {
|
||||
) { progressCallback, _ ->
|
||||
try {
|
||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
||||
FileUtil.unzipToInternalStorage(
|
||||
result.toString(),
|
||||
cacheSaveDir,
|
||||
progressCallback
|
||||
)
|
||||
val files = cacheSaveDir.listFiles()
|
||||
var successfulImports = 0
|
||||
var failedImports = 0
|
||||
@ -287,7 +280,7 @@ class InstallableFragment : Fragment() {
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
private val exportSaves = registerForActivityResult(
|
||||
@ -297,11 +290,11 @@ class InstallableFragment : Fragment() {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.save_files_exporting,
|
||||
false
|
||||
) {
|
||||
) { _, _ ->
|
||||
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||
cacheSaveDir.mkdir()
|
||||
|
||||
@ -338,6 +331,6 @@ class InstallableFragment : Fragment() {
|
||||
TaskState.Completed -> getString(R.string.export_success)
|
||||
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
||||
}
|
||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,15 @@ class MessageDialogFragment : DialogFragment() {
|
||||
val descriptionId = requireArguments().getInt(DESCRIPTION_ID)
|
||||
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
|
||||
val helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||
val dismissible = requireArguments().getBoolean(DISMISSIBLE)
|
||||
val clearPositiveAction = requireArguments().getBoolean(CLEAR_POSITIVE_ACTION)
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
|
||||
if (clearPositiveAction) {
|
||||
messageDialogViewModel.positiveAction = null
|
||||
}
|
||||
|
||||
if (messageDialogViewModel.positiveAction == null) {
|
||||
builder.setPositiveButton(R.string.close, null)
|
||||
} else {
|
||||
@ -51,6 +57,8 @@ class MessageDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
isCancelable = dismissible
|
||||
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
@ -67,28 +75,38 @@ class MessageDialogFragment : DialogFragment() {
|
||||
private const val DESCRIPTION_ID = "DescriptionId"
|
||||
private const val DESCRIPTION_STRING = "DescriptionString"
|
||||
private const val HELP_LINK = "Link"
|
||||
private const val DISMISSIBLE = "Dismissible"
|
||||
private const val CLEAR_POSITIVE_ACTION = "ClearPositiveAction"
|
||||
|
||||
fun newInstance(
|
||||
activity: FragmentActivity,
|
||||
activity: FragmentActivity? = null,
|
||||
titleId: Int = 0,
|
||||
titleString: String = "",
|
||||
descriptionId: Int = 0,
|
||||
descriptionString: String = "",
|
||||
helpLinkId: Int = 0,
|
||||
dismissible: Boolean = true,
|
||||
positiveAction: (() -> Unit)? = null
|
||||
): MessageDialogFragment {
|
||||
var clearPositiveAction = false
|
||||
if (activity != null) {
|
||||
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
|
||||
clear()
|
||||
this.positiveAction = positiveAction
|
||||
}
|
||||
} else {
|
||||
clearPositiveAction = true
|
||||
}
|
||||
|
||||
val dialog = MessageDialogFragment()
|
||||
val bundle = Bundle()
|
||||
bundle.apply {
|
||||
val bundle = Bundle().apply {
|
||||
putInt(TITLE_ID, titleId)
|
||||
putString(TITLE_STRING, titleString)
|
||||
putInt(DESCRIPTION_ID, descriptionId)
|
||||
putString(DESCRIPTION_STRING, descriptionString)
|
||||
putInt(HELP_LINK, helpLinkId)
|
||||
}
|
||||
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
|
||||
clear()
|
||||
this.positiveAction = positiveAction
|
||||
putBoolean(DISMISSIBLE, dismissible)
|
||||
putBoolean(CLEAR_POSITIVE_ACTION, clearPositiveAction)
|
||||
}
|
||||
dialog.arguments = bundle
|
||||
return dialog
|
||||
|
@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
|
||||
import org.yuzu.yuzu_emu.model.TaskViewModel
|
||||
|
||||
class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||
class ProgressDialogFragment : DialogFragment() {
|
||||
private val taskViewModel: TaskViewModel by activityViewModels()
|
||||
|
||||
private lateinit var binding: DialogProgressBarBinding
|
||||
|
||||
private val PROGRESS_BAR_RESOLUTION = 1000
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val titleId = requireArguments().getInt(TITLE)
|
||||
val cancellable = requireArguments().getBoolean(CANCELLABLE)
|
||||
@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.message.isSelected = true
|
||||
viewLifecycleOwner.lifecycleScope.apply {
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
taskViewModel.progress.collect {
|
||||
if (it != 0.0) {
|
||||
binding.progressBar.apply {
|
||||
isIndeterminate = false
|
||||
progress = (
|
||||
(it / taskViewModel.maxProgress.value) *
|
||||
PROGRESS_BAR_RESOLUTION
|
||||
).toInt()
|
||||
min = 0
|
||||
max = PROGRESS_BAR_RESOLUTION
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
taskViewModel.message.collect {
|
||||
if (it.isEmpty()) {
|
||||
binding.message.visibility = View.GONE
|
||||
} else {
|
||||
binding.message.visibility = View.VISIBLE
|
||||
binding.message.text = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||
val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
alertDialog.setTitle(getString(R.string.cancelling))
|
||||
binding.progressBar.isIndeterminate = true
|
||||
taskViewModel.setCancelled(true)
|
||||
}
|
||||
}
|
||||
@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||
activity: FragmentActivity,
|
||||
titleId: Int,
|
||||
cancellable: Boolean = false,
|
||||
task: suspend () -> Any
|
||||
): IndeterminateProgressDialogFragment {
|
||||
val dialog = IndeterminateProgressDialogFragment()
|
||||
task: suspend (
|
||||
progressCallback: (max: Long, progress: Long) -> Boolean,
|
||||
messageCallback: (message: String) -> Unit
|
||||
) -> Any
|
||||
): ProgressDialogFragment {
|
||||
val dialog = ProgressDialogFragment()
|
||||
val args = Bundle()
|
||||
ViewModelProvider(activity)[TaskViewModel::class.java].task = task
|
||||
args.putInt(TITLE, titleId)
|
@ -136,14 +136,14 @@ class SearchFragment : Fragment() {
|
||||
baseList.filter {
|
||||
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
|
||||
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
||||
}
|
||||
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
|
||||
}
|
||||
|
||||
R.id.chip_recently_added -> {
|
||||
baseList.filter {
|
||||
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
|
||||
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
||||
}
|
||||
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
|
||||
}
|
||||
|
||||
R.id.chip_homebrew -> baseList.filter { it.isHomebrew }
|
||||
|
@ -31,6 +31,7 @@ import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import kotlinx.coroutines.launch
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import java.io.File
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
@ -162,7 +163,7 @@ class SetupFragment : Fragment() {
|
||||
R.string.install_prod_keys_warning_help,
|
||||
{
|
||||
val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys")
|
||||
if (file.exists()) {
|
||||
if (file.exists() && NativeLibrary.areKeysPresent()) {
|
||||
StepState.COMPLETE
|
||||
} else {
|
||||
StepState.INCOMPLETE
|
||||
@ -347,7 +348,8 @@ class SetupFragment : Fragment() {
|
||||
val getProdKey =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
if (result != null) {
|
||||
if (mainActivity.processKey(result)) {
|
||||
mainActivity.processKey(result)
|
||||
if (NativeLibrary.areKeysPresent()) {
|
||||
keyCallback.onStepCompleted()
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
data class Addon(
|
||||
var enabled: Boolean,
|
||||
val title: String,
|
||||
val version: String
|
||||
)
|
@ -15,8 +15,8 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class AddonViewModel : ViewModel() {
|
||||
private val _addonList = MutableStateFlow(mutableListOf<Addon>())
|
||||
val addonList get() = _addonList.asStateFlow()
|
||||
private val _patchList = MutableStateFlow(mutableListOf<Patch>())
|
||||
val addonList get() = _patchList.asStateFlow()
|
||||
|
||||
private val _showModInstallPicker = MutableStateFlow(false)
|
||||
val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
|
||||
@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() {
|
||||
private val _showModNoticeDialog = MutableStateFlow(false)
|
||||
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
|
||||
|
||||
private val _addonToDelete = MutableStateFlow<Patch?>(null)
|
||||
val addonToDelete = _addonToDelete.asStateFlow()
|
||||
|
||||
var game: Game? = null
|
||||
|
||||
private val isRefreshing = AtomicBoolean(false)
|
||||
@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() {
|
||||
isRefreshing.set(true)
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val addonList = mutableListOf<Addon>()
|
||||
val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
|
||||
NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
|
||||
val name = it.first.replace("[D] ", "")
|
||||
addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
|
||||
}
|
||||
addonList.sortBy { it.title }
|
||||
_addonList.value = addonList
|
||||
val patchList = (
|
||||
NativeLibrary.getPatchesForFile(game!!.path, game!!.programId)
|
||||
?: emptyArray()
|
||||
).toMutableList()
|
||||
patchList.sortBy { it.name }
|
||||
_patchList.value = patchList
|
||||
isRefreshing.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAddonToDelete(patch: Patch?) {
|
||||
_addonToDelete.value = patch
|
||||
}
|
||||
|
||||
fun onDeleteAddon(patch: Patch) {
|
||||
when (PatchType.from(patch.type)) {
|
||||
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
|
||||
PatchType.DLC -> NativeLibrary.removeDLC(patch.programId)
|
||||
PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name)
|
||||
}
|
||||
refreshAddons()
|
||||
}
|
||||
|
||||
fun onCloseAddons() {
|
||||
if (_addonList.value.isEmpty()) {
|
||||
if (_patchList.value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
NativeConfig.setDisabledAddons(
|
||||
game!!.programId,
|
||||
_addonList.value.mapNotNull {
|
||||
_patchList.value.mapNotNull {
|
||||
if (it.enabled) {
|
||||
null
|
||||
} else {
|
||||
it.title
|
||||
it.name
|
||||
}
|
||||
}.toTypedArray()
|
||||
)
|
||||
NativeConfig.saveGlobalConfig()
|
||||
_addonList.value.clear()
|
||||
_patchList.value.clear()
|
||||
game = null
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
|
||||
|
||||
data class Driver(
|
||||
override var selected: Boolean,
|
||||
val title: String,
|
||||
val version: String = "",
|
||||
val description: String = ""
|
||||
) : SelectableItem {
|
||||
override fun onSelectionStateChanged(selected: Boolean) {
|
||||
this.selected = selected
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun GpuDriverMetadata.toDriver(selected: Boolean = false): Driver =
|
||||
Driver(
|
||||
selected,
|
||||
this.name ?: "",
|
||||
this.version ?: "",
|
||||
this.description ?: ""
|
||||
)
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
@ -17,11 +18,10 @@ import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
|
||||
class DriverViewModel : ViewModel() {
|
||||
@ -38,97 +38,81 @@ class DriverViewModel : ViewModel() {
|
||||
!loading && ready && !deleting
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
|
||||
|
||||
private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers())
|
||||
val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
|
||||
var driverData = GpuDriverHelper.getDrivers()
|
||||
|
||||
var previouslySelectedDriver = 0
|
||||
var selectedDriver = -1
|
||||
private val _driverList = MutableStateFlow(emptyList<Driver>())
|
||||
val driverList: StateFlow<List<Driver>> get() = _driverList
|
||||
|
||||
// Used for showing which driver is currently installed within the driver manager card
|
||||
private val _selectedDriverTitle = MutableStateFlow("")
|
||||
val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
|
||||
|
||||
private val _newDriverInstalled = MutableStateFlow(false)
|
||||
val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
|
||||
private val _showClearButton = MutableStateFlow(false)
|
||||
val showClearButton = _showClearButton.asStateFlow()
|
||||
|
||||
val driversToDelete = mutableListOf<String>()
|
||||
private val driversToDelete = mutableListOf<String>()
|
||||
|
||||
init {
|
||||
val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData
|
||||
findSelectedDriver(currentDriverMetadata)
|
||||
|
||||
// If a user had installed a driver before the manager was implemented, this zips
|
||||
// the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
|
||||
// be indexed and exported as expected.
|
||||
if (selectedDriver == -1) {
|
||||
val driverToSave =
|
||||
File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
|
||||
driverToSave.createNewFile()
|
||||
FileUtil.zipFromInternalStorage(
|
||||
File(GpuDriverHelper.driverInstallationPath!!),
|
||||
GpuDriverHelper.driverInstallationPath!!,
|
||||
BufferedOutputStream(driverToSave.outputStream())
|
||||
)
|
||||
_driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
|
||||
setSelectedDriverIndex(_driverList.value.size - 1)
|
||||
}
|
||||
|
||||
// If a user had installed a driver before the config was reworked to be multiplatform,
|
||||
// we have save the path of the previously selected driver to the new setting.
|
||||
if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 &&
|
||||
StringSetting.DRIVER_PATH.global
|
||||
) {
|
||||
StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first)
|
||||
NativeConfig.saveGlobalConfig()
|
||||
} else {
|
||||
findSelectedDriver(GpuDriverHelper.customDriverSettingData)
|
||||
}
|
||||
updateDriverList()
|
||||
updateDriverNameForGame(null)
|
||||
}
|
||||
|
||||
fun setSelectedDriverIndex(value: Int) {
|
||||
if (selectedDriver != -1) {
|
||||
previouslySelectedDriver = selectedDriver
|
||||
fun reloadDriverData() {
|
||||
_areDriversLoading.value = true
|
||||
driverData = GpuDriverHelper.getDrivers()
|
||||
updateDriverList()
|
||||
_areDriversLoading.value = false
|
||||
}
|
||||
|
||||
fun updateDriverList() {
|
||||
val selectedDriver = GpuDriverHelper.customDriverSettingData
|
||||
val newDriverList = mutableListOf(
|
||||
Driver(
|
||||
selectedDriver == GpuDriverMetadata(),
|
||||
YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||
)
|
||||
)
|
||||
driverData.forEach {
|
||||
newDriverList.add(it.second.toDriver(it.second == selectedDriver))
|
||||
}
|
||||
selectedDriver = value
|
||||
}
|
||||
|
||||
fun setNewDriverInstalled(value: Boolean) {
|
||||
_newDriverInstalled.value = value
|
||||
}
|
||||
|
||||
fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
|
||||
val driverIndex = _driverList.value.indexOfFirst { it == driverData }
|
||||
if (driverIndex == -1) {
|
||||
_driverList.value.add(driverData)
|
||||
setSelectedDriverIndex(_driverList.value.size - 1)
|
||||
_selectedDriverTitle.value = driverData.second.name
|
||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||
} else {
|
||||
setSelectedDriverIndex(driverIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) {
|
||||
_driverList.value.remove(driverData)
|
||||
_driverList.value = newDriverList
|
||||
}
|
||||
|
||||
fun onOpenDriverManager(game: Game?) {
|
||||
if (game != null) {
|
||||
SettingsFile.loadCustomConfig(game)
|
||||
}
|
||||
updateDriverList()
|
||||
}
|
||||
|
||||
val driverPath = StringSetting.DRIVER_PATH.getString()
|
||||
if (driverPath.isEmpty()) {
|
||||
setSelectedDriverIndex(0)
|
||||
fun showClearButton(value: Boolean) {
|
||||
_showClearButton.value = value
|
||||
}
|
||||
|
||||
fun onDriverSelected(position: Int) {
|
||||
if (position == 0) {
|
||||
StringSetting.DRIVER_PATH.setString("")
|
||||
} else {
|
||||
findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
|
||||
StringSetting.DRIVER_PATH.setString(driverData[position - 1].first)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDriverRemoved(removedPosition: Int, selectedPosition: Int) {
|
||||
driversToDelete.add(driverData[removedPosition - 1].first)
|
||||
driverData.removeAt(removedPosition - 1)
|
||||
onDriverSelected(selectedPosition)
|
||||
}
|
||||
|
||||
fun onDriverAdded(driver: Pair<String, GpuDriverMetadata>) {
|
||||
if (driversToDelete.contains(driver.first)) {
|
||||
driversToDelete.remove(driver.first)
|
||||
}
|
||||
driverData.add(driver)
|
||||
onDriverSelected(driverData.size)
|
||||
}
|
||||
|
||||
fun onCloseDriverManager(game: Game?) {
|
||||
_isDeletingDrivers.value = true
|
||||
StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
|
||||
updateDriverNameForGame(game)
|
||||
if (game == null) {
|
||||
NativeConfig.saveGlobalConfig()
|
||||
@ -160,6 +144,7 @@ class DriverViewModel : ViewModel() {
|
||||
val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString())
|
||||
val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData
|
||||
if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) {
|
||||
setDriverReady()
|
||||
return
|
||||
}
|
||||
|
||||
@ -181,20 +166,6 @@ class DriverViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) {
|
||||
if (driverList.value.size == 1) {
|
||||
setSelectedDriverIndex(0)
|
||||
return
|
||||
}
|
||||
|
||||
driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> ->
|
||||
if (driver.second == currentDriverMetadata) {
|
||||
setSelectedDriverIndex(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDriverNameForGame(game: Game?) {
|
||||
if (!GpuDriverHelper.supportsCustomDriverLoading()) {
|
||||
return
|
||||
@ -217,7 +188,6 @@ class DriverViewModel : ViewModel() {
|
||||
|
||||
private fun setDriverReady() {
|
||||
_isDriverReady.value = true
|
||||
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
|
||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||
updateName()
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package org.yuzu.yuzu_emu.model
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class EmulationViewModel : ViewModel() {
|
||||
val emulationStarted: StateFlow<Boolean> get() = _emulationStarted
|
||||
@ -23,6 +24,9 @@ class EmulationViewModel : ViewModel() {
|
||||
val shaderMessage: StateFlow<String> get() = _shaderMessage
|
||||
private val _shaderMessage = MutableStateFlow("")
|
||||
|
||||
private val _drawerOpen = MutableStateFlow(false)
|
||||
val drawerOpen = _drawerOpen.asStateFlow()
|
||||
|
||||
fun setEmulationStarted(started: Boolean) {
|
||||
_emulationStarted.value = started
|
||||
}
|
||||
@ -49,6 +53,10 @@ class EmulationViewModel : ViewModel() {
|
||||
setTotalShaders(max)
|
||||
}
|
||||
|
||||
fun setDrawerOpen(value: Boolean) {
|
||||
_drawerOpen.value = value
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
setEmulationStarted(false)
|
||||
setIsEmulationStopping(false)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import java.util.HashSet
|
||||
@ -11,6 +12,7 @@ import kotlinx.serialization.Serializable
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import java.time.LocalDateTime
|
||||
@ -61,12 +63,26 @@ class Game(
|
||||
val addonDir: String
|
||||
get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Game) {
|
||||
return false
|
||||
val launchIntent: Intent
|
||||
get() = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse(path)
|
||||
}
|
||||
|
||||
return hashCode() == other.hashCode()
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Game
|
||||
|
||||
if (title != other.title) return false
|
||||
if (path != other.path) return false
|
||||
if (programId != other.programId) return false
|
||||
if (developer != other.developer) return false
|
||||
if (version != other.version) return false
|
||||
if (isHomebrew != other.isHomebrew) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
enum class GameVerificationResult(val int: Int) {
|
||||
Success(0),
|
||||
Failed(1),
|
||||
NotImplemented(2);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): GameVerificationResult =
|
||||
entries.firstOrNull { it.int == int } ?: Success
|
||||
}
|
||||
}
|
@ -31,6 +31,9 @@ class HomeViewModel : ViewModel() {
|
||||
private val _reloadPropertiesList = MutableStateFlow(false)
|
||||
val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
|
||||
|
||||
private val _checkKeys = MutableStateFlow(false)
|
||||
val checkKeys = _checkKeys.asStateFlow()
|
||||
|
||||
var navigatedToSetup = false
|
||||
|
||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||
@ -66,4 +69,8 @@ class HomeViewModel : ViewModel() {
|
||||
fun reloadPropertiesList(reload: Boolean) {
|
||||
_reloadPropertiesList.value = reload
|
||||
}
|
||||
|
||||
fun setCheckKeys(value: Boolean) {
|
||||
_checkKeys.value = value
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
enum class InstallResult(val int: Int) {
|
||||
Success(0),
|
||||
Overwrite(1),
|
||||
Failure(2),
|
||||
BaseInstallAttempted(3);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
data class Patch(
|
||||
var enabled: Boolean,
|
||||
val name: String,
|
||||
val version: String,
|
||||
val type: Int,
|
||||
val programId: String,
|
||||
val titleId: String
|
||||
)
|
@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
enum class PatchType(val int: Int) {
|
||||
Update(0),
|
||||
DLC(1),
|
||||
Mod(2);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
interface SelectableItem {
|
||||
var selected: Boolean
|
||||
fun onSelectionStateChanged(selected: Boolean)
|
||||
}
|
@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TaskViewModel : ViewModel() {
|
||||
@ -23,13 +24,28 @@ class TaskViewModel : ViewModel() {
|
||||
val cancelled: StateFlow<Boolean> get() = _cancelled
|
||||
private val _cancelled = MutableStateFlow(false)
|
||||
|
||||
lateinit var task: suspend () -> Any
|
||||
private val _progress = MutableStateFlow(0.0)
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
private val _maxProgress = MutableStateFlow(0.0)
|
||||
val maxProgress = _maxProgress.asStateFlow()
|
||||
|
||||
private val _message = MutableStateFlow("")
|
||||
val message = _message.asStateFlow()
|
||||
|
||||
lateinit var task: suspend (
|
||||
progressCallback: (max: Long, progress: Long) -> Boolean,
|
||||
messageCallback: (message: String) -> Unit
|
||||
) -> Any
|
||||
|
||||
fun clear() {
|
||||
_result.value = Any()
|
||||
_isComplete.value = false
|
||||
_isRunning.value = false
|
||||
_cancelled.value = false
|
||||
_progress.value = 0.0
|
||||
_maxProgress.value = 0.0
|
||||
_message.value = ""
|
||||
}
|
||||
|
||||
fun setCancelled(value: Boolean) {
|
||||
@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() {
|
||||
_isRunning.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val res = task()
|
||||
val res = task(
|
||||
{ max, progress ->
|
||||
_maxProgress.value = max.toDouble()
|
||||
_progress.value = progress.toDouble()
|
||||
return@task cancelled.value
|
||||
},
|
||||
{ message ->
|
||||
_message.value = message
|
||||
}
|
||||
)
|
||||
_result.value = res
|
||||
_isComplete.value = true
|
||||
_isRunning.value = false
|
||||
|
@ -38,11 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||
import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
|
||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.model.InstallResult
|
||||
import org.yuzu.yuzu_emu.model.TaskState
|
||||
import org.yuzu.yuzu_emu.model.TaskViewModel
|
||||
import org.yuzu.yuzu_emu.utils.*
|
||||
@ -58,9 +60,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
private val gamesViewModel: GamesViewModel by viewModels()
|
||||
private val taskViewModel: TaskViewModel by viewModels()
|
||||
private val addonViewModel: AddonViewModel by viewModels()
|
||||
private val driverViewModel: DriverViewModel by viewModels()
|
||||
|
||||
override var themeId: Int = 0
|
||||
|
||||
private val CHECKED_DECRYPTION = "CheckedDecryption"
|
||||
private var checkedDecryption = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
||||
@ -72,6 +78,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
checkedDecryption = savedInstanceState.getBoolean(CHECKED_DECRYPTION)
|
||||
}
|
||||
if (!checkedDecryption) {
|
||||
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||
if (!firstTimeSetup) {
|
||||
checkKeys()
|
||||
}
|
||||
checkedDecryption = true
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||
|
||||
@ -147,6 +165,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
homeViewModel.checkKeys.collect {
|
||||
if (it) {
|
||||
checkKeys()
|
||||
homeViewModel.setCheckKeys(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss previous notifications (should not happen unless a crash occurred)
|
||||
@ -155,6 +183,21 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
setInsets()
|
||||
}
|
||||
|
||||
private fun checkKeys() {
|
||||
if (!NativeLibrary.areKeysPresent()) {
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.keys_missing,
|
||||
descriptionId = R.string.keys_missing_description,
|
||||
helpLinkId = R.string.keys_missing_help
|
||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(CHECKED_DECRYPTION, checkedDecryption)
|
||||
}
|
||||
|
||||
fun finishSetup(navController: NavController) {
|
||||
navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
|
||||
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
|
||||
@ -346,6 +389,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
R.string.install_keys_success,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
homeViewModel.setCheckKeys(true)
|
||||
gamesViewModel.reloadGames(true)
|
||||
return true
|
||||
} else {
|
||||
@ -367,26 +411,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val inputZip = contentResolver.openInputStream(result)
|
||||
if (inputZip == null) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.fatal_error),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
|
||||
|
||||
val firmwarePath =
|
||||
File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
|
||||
val cacheFirmwareDir = File("${cacheDir.path}/registered/")
|
||||
|
||||
val task: () -> Any = {
|
||||
ProgressDialogFragment.newInstance(
|
||||
this,
|
||||
R.string.firmware_installing
|
||||
) { progressCallback, _ ->
|
||||
var messageToShow: Any
|
||||
try {
|
||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
|
||||
FileUtil.unzipToInternalStorage(
|
||||
result.toString(),
|
||||
cacheFirmwareDir,
|
||||
progressCallback
|
||||
)
|
||||
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
|
||||
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
|
||||
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
|
||||
@ -399,21 +440,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
firmwarePath.deleteRecursively()
|
||||
cacheFirmwareDir.copyRecursively(firmwarePath, true)
|
||||
NativeLibrary.initializeSystem(true)
|
||||
homeViewModel.setCheckKeys(true)
|
||||
getString(R.string.save_file_imported_success)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.error("[MainActivity] Firmware install failed - ${e.message}")
|
||||
messageToShow = getString(R.string.fatal_error)
|
||||
} finally {
|
||||
cacheFirmwareDir.deleteRecursively()
|
||||
}
|
||||
messageToShow
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
this,
|
||||
R.string.firmware_installing,
|
||||
task = task
|
||||
).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
val getAmiiboKey =
|
||||
@ -472,11 +509,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
this@MainActivity,
|
||||
R.string.verifying_content,
|
||||
false
|
||||
) {
|
||||
) { _, _ ->
|
||||
var updatesMatchProgram = true
|
||||
for (document in documents) {
|
||||
val valid = NativeLibrary.doesUpdateMatchProgram(
|
||||
@ -499,44 +536,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
positiveAction = { homeViewModel.setContentToInstall(documents) }
|
||||
)
|
||||
}
|
||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
private fun installContent(documents: List<Uri>) {
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
this@MainActivity,
|
||||
R.string.installing_game_content
|
||||
) {
|
||||
) { progressCallback, messageCallback ->
|
||||
var installSuccess = 0
|
||||
var installOverwrite = 0
|
||||
var errorBaseGame = 0
|
||||
var errorExtension = 0
|
||||
var errorOther = 0
|
||||
var error = 0
|
||||
documents.forEach {
|
||||
messageCallback.invoke(FileUtil.getFilename(it))
|
||||
when (
|
||||
NativeLibrary.installFileToNand(
|
||||
it.toString(),
|
||||
FileUtil.getExtension(it)
|
||||
InstallResult.from(
|
||||
NativeLibrary.installFileToNand(
|
||||
it.toString(),
|
||||
progressCallback
|
||||
)
|
||||
)
|
||||
) {
|
||||
NativeLibrary.InstallFileToNandResult.Success -> {
|
||||
InstallResult.Success -> {
|
||||
installSuccess += 1
|
||||
}
|
||||
|
||||
NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
|
||||
InstallResult.Overwrite -> {
|
||||
installOverwrite += 1
|
||||
}
|
||||
|
||||
NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
|
||||
InstallResult.BaseInstallAttempted -> {
|
||||
errorBaseGame += 1
|
||||
}
|
||||
|
||||
NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
|
||||
errorExtension += 1
|
||||
}
|
||||
|
||||
else -> {
|
||||
errorOther += 1
|
||||
InstallResult.Failure -> {
|
||||
error += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -563,7 +598,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
)
|
||||
installResult.append(separator)
|
||||
}
|
||||
val errorTotal: Int = errorBaseGame + errorExtension + errorOther
|
||||
val errorTotal: Int = errorBaseGame + error
|
||||
if (errorTotal > 0) {
|
||||
installResult.append(separator)
|
||||
installResult.append(
|
||||
@ -580,14 +615,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
)
|
||||
installResult.append(separator)
|
||||
}
|
||||
if (errorExtension > 0) {
|
||||
installResult.append(separator)
|
||||
installResult.append(
|
||||
getString(R.string.install_game_content_failure_file_extension)
|
||||
)
|
||||
installResult.append(separator)
|
||||
}
|
||||
if (errorOther > 0) {
|
||||
if (error > 0) {
|
||||
installResult.append(
|
||||
getString(R.string.install_game_content_failure_description)
|
||||
)
|
||||
@ -606,7 +634,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
descriptionString = installResult.toString().trim()
|
||||
)
|
||||
}
|
||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
val exportUserData = registerForActivityResult(
|
||||
@ -616,16 +644,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
this,
|
||||
R.string.exporting_user_data,
|
||||
true
|
||||
) {
|
||||
) { progressCallback, _ ->
|
||||
val zipResult = FileUtil.zipFromInternalStorage(
|
||||
File(DirectoryInitialization.userDirectory!!),
|
||||
DirectoryInitialization.userDirectory!!,
|
||||
BufferedOutputStream(contentResolver.openOutputStream(result)),
|
||||
taskViewModel.cancelled,
|
||||
progressCallback,
|
||||
compression = false
|
||||
)
|
||||
return@newInstance when (zipResult) {
|
||||
@ -633,7 +661,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
TaskState.Failed -> R.string.export_failed
|
||||
TaskState.Cancelled -> R.string.user_data_export_cancelled
|
||||
}
|
||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
val importUserData =
|
||||
@ -642,10 +670,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
IndeterminateProgressDialogFragment.newInstance(
|
||||
ProgressDialogFragment.newInstance(
|
||||
this,
|
||||
R.string.importing_user_data
|
||||
) {
|
||||
) { progressCallback, _ ->
|
||||
val checkStream =
|
||||
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
|
||||
var isYuzuBackup = false
|
||||
@ -674,8 +702,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
// Copy archive to internal storage
|
||||
try {
|
||||
FileUtil.unzipToInternalStorage(
|
||||
BufferedInputStream(contentResolver.openInputStream(result)),
|
||||
File(DirectoryInitialization.userDirectory!!)
|
||||
result.toString(),
|
||||
File(DirectoryInitialization.userDirectory!!),
|
||||
progressCallback
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return@newInstance MessageDialogFragment.newInstance(
|
||||
@ -689,8 +718,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
NativeLibrary.initializeSystem(true)
|
||||
NativeConfig.initializeGlobalConfig()
|
||||
gamesViewModel.reloadGames(false)
|
||||
driverViewModel.reloadDriverData()
|
||||
|
||||
return@newInstance getString(R.string.user_data_import_success)
|
||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@ -19,6 +18,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||
import org.yuzu.yuzu_emu.model.TaskState
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.NullPointerException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.Deflater
|
||||
@ -104,7 +104,7 @@ object FileUtil {
|
||||
|
||||
/**
|
||||
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
|
||||
* This function will be faster than DoucmentFile.listFiles
|
||||
* This function will be faster than DocumentFile.listFiles
|
||||
* @param uri Directory uri.
|
||||
* @return CheapDocument lists.
|
||||
*/
|
||||
@ -283,12 +283,34 @@ object FileUtil {
|
||||
|
||||
/**
|
||||
* Extracts the given zip file into the given directory.
|
||||
* @param path String representation of a [Uri] or a typical path delimited by '/'
|
||||
* @param destDir Location to unzip the contents of [path] into
|
||||
* @param progressCallback Lambda that is called with the total number of files and the current
|
||||
* progress through the process. Stops execution as soon as possible if this returns true.
|
||||
*/
|
||||
@Throws(SecurityException::class)
|
||||
fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
|
||||
ZipInputStream(zipStream).use { zis ->
|
||||
fun unzipToInternalStorage(
|
||||
path: String,
|
||||
destDir: File,
|
||||
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
|
||||
) {
|
||||
var totalEntries = 0L
|
||||
ZipInputStream(getInputStream(path)).use { zis ->
|
||||
var tempEntry = zis.nextEntry
|
||||
while (tempEntry != null) {
|
||||
tempEntry = zis.nextEntry
|
||||
totalEntries++
|
||||
}
|
||||
}
|
||||
|
||||
var progress = 0L
|
||||
ZipInputStream(getInputStream(path)).use { zis ->
|
||||
var entry: ZipEntry? = zis.nextEntry
|
||||
while (entry != null) {
|
||||
if (progressCallback.invoke(totalEntries, progress)) {
|
||||
return@use
|
||||
}
|
||||
|
||||
val newFile = File(destDir, entry.name)
|
||||
val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
|
||||
|
||||
@ -304,6 +326,7 @@ object FileUtil {
|
||||
newFile.outputStream().use { fos -> zis.copyTo(fos) }
|
||||
}
|
||||
entry = zis.nextEntry
|
||||
progress++
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -313,14 +336,15 @@ object FileUtil {
|
||||
* @param inputFile File representation of the item that will be zipped
|
||||
* @param rootDir Directory containing the inputFile
|
||||
* @param outputStream Stream where the zip file will be output
|
||||
* @param cancelled [StateFlow] that reports whether this process has been cancelled
|
||||
* @param progressCallback Lambda that is called with the total number of files and the current
|
||||
* progress through the process. Stops execution as soon as possible if this returns true.
|
||||
* @param compression Disables compression if true
|
||||
*/
|
||||
fun zipFromInternalStorage(
|
||||
inputFile: File,
|
||||
rootDir: String,
|
||||
outputStream: BufferedOutputStream,
|
||||
cancelled: StateFlow<Boolean>? = null,
|
||||
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false },
|
||||
compression: Boolean = true
|
||||
): TaskState {
|
||||
try {
|
||||
@ -330,8 +354,10 @@ object FileUtil {
|
||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||
}
|
||||
|
||||
var count = 0L
|
||||
val totalFiles = inputFile.walkTopDown().count().toLong()
|
||||
inputFile.walkTopDown().forEach { file ->
|
||||
if (cancelled?.value == true) {
|
||||
if (progressCallback.invoke(totalFiles, count)) {
|
||||
return TaskState.Cancelled
|
||||
}
|
||||
|
||||
@ -343,6 +369,7 @@ object FileUtil {
|
||||
if (file.isFile) {
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -356,9 +383,14 @@ object FileUtil {
|
||||
/**
|
||||
* Helper function that copies the contents of a DocumentFile folder into a [File]
|
||||
* @param file [File] representation of the folder to copy into
|
||||
* @param progressCallback Lambda that is called with the total number of files and the current
|
||||
* progress through the process. Stops execution as soon as possible if this returns true.
|
||||
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
|
||||
*/
|
||||
fun DocumentFile.copyFilesTo(file: File) {
|
||||
fun DocumentFile.copyFilesTo(
|
||||
file: File,
|
||||
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
|
||||
) {
|
||||
file.mkdirs()
|
||||
if (!this.isDirectory || !file.isDirectory) {
|
||||
throw IllegalStateException(
|
||||
@ -366,7 +398,13 @@ object FileUtil {
|
||||
)
|
||||
}
|
||||
|
||||
var count = 0L
|
||||
val totalFiles = this.listFiles().size.toLong()
|
||||
this.listFiles().forEach {
|
||||
if (progressCallback.invoke(totalFiles, count)) {
|
||||
return
|
||||
}
|
||||
|
||||
val newFile = File(file, it.name!!)
|
||||
if (it.isDirectory) {
|
||||
newFile.mkdirs()
|
||||
@ -381,6 +419,7 @@ object FileUtil {
|
||||
newFile.outputStream().use { os -> bos.copyTo(os) }
|
||||
}
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
@ -427,6 +466,18 @@ object FileUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun getInputStream(path: String) = if (path.contains("content://")) {
|
||||
Uri.parse(path).inputStream()
|
||||
} else {
|
||||
File(path).inputStream()
|
||||
}
|
||||
|
||||
fun getOutputStream(path: String) = if (path.contains("content://")) {
|
||||
Uri.parse(path).outputStream()
|
||||
} else {
|
||||
File(path).outputStream()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getStringFromFile(file: File): String =
|
||||
String(file.readBytes(), StandardCharsets.UTF_8)
|
||||
@ -434,4 +485,19 @@ object FileUtil {
|
||||
@Throws(IOException::class)
|
||||
fun getStringFromInputStream(stream: InputStream): String =
|
||||
String(stream.readBytes(), StandardCharsets.UTF_8)
|
||||
|
||||
fun DocumentFile.inputStream(): InputStream =
|
||||
YuzuApplication.appContext.contentResolver.openInputStream(uri)!!
|
||||
|
||||
fun DocumentFile.outputStream(): OutputStream =
|
||||
YuzuApplication.appContext.contentResolver.openOutputStream(uri)!!
|
||||
|
||||
fun Uri.inputStream(): InputStream =
|
||||
YuzuApplication.appContext.contentResolver.openInputStream(this)!!
|
||||
|
||||
fun Uri.outputStream(): OutputStream =
|
||||
YuzuApplication.appContext.contentResolver.openOutputStream(this)!!
|
||||
|
||||
fun Uri.asDocumentFile(): DocumentFile? =
|
||||
DocumentFile.fromSingleUri(YuzuApplication.appContext, this)
|
||||
}
|
||||
|
@ -5,7 +5,10 @@ package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@ -85,4 +88,22 @@ object GameIconUtils {
|
||||
return imageLoader.execute(request)
|
||||
.drawable!!.toBitmap(config = Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
|
||||
suspend fun getShortcutIcon(lifecycleOwner: LifecycleOwner, game: Game): IconCompat {
|
||||
val layerDrawable = ResourcesCompat.getDrawable(
|
||||
YuzuApplication.appContext.resources,
|
||||
R.drawable.shortcut,
|
||||
null
|
||||
) as LayerDrawable
|
||||
layerDrawable.setDrawableByLayerId(
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
@ -62,9 +61,6 @@ object GpuDriverHelper {
|
||||
?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
|
||||
?.distinct()
|
||||
?.toMutableList() ?: mutableListOf()
|
||||
|
||||
// TODO: Get system driver information
|
||||
drivers.add(0, Pair("", GpuDriverMetadata()))
|
||||
return drivers
|
||||
}
|
||||
|
||||
@ -126,7 +122,7 @@ object GpuDriverHelper {
|
||||
// Unzip the driver.
|
||||
try {
|
||||
FileUtil.unzipToInternalStorage(
|
||||
BufferedInputStream(copiedFile.inputStream()),
|
||||
copiedFile.path,
|
||||
File(driverInstallationPath!!)
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
@ -159,7 +155,7 @@ object GpuDriverHelper {
|
||||
// Unzip the driver to the private installation directory
|
||||
try {
|
||||
FileUtil.unzipToInternalStorage(
|
||||
BufferedInputStream(driver.inputStream()),
|
||||
driver.path,
|
||||
File(driverInstallationPath!!)
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
|
@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.viewholder
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.yuzu.yuzu_emu.adapters.AbstractDiffAdapter
|
||||
import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
|
||||
|
||||
/**
|
||||
* [RecyclerView.ViewHolder] meant to work together with a [AbstractDiffAdapter] or a
|
||||
* [AbstractListAdapter] so we can run [bind] on each list item without needing a manual hookup.
|
||||
*/
|
||||
abstract class AbstractViewHolder<Model>(binding: ViewBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
abstract fun bind(model: Model)
|
||||
}
|
@ -42,3 +42,19 @@ double GetJDouble(JNIEnv* env, jobject jdouble) {
|
||||
jobject ToJDouble(JNIEnv* env, double value) {
|
||||
return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
|
||||
}
|
||||
|
||||
s32 GetJInteger(JNIEnv* env, jobject jinteger) {
|
||||
return env->GetIntField(jinteger, IDCache::GetIntegerValueField());
|
||||
}
|
||||
|
||||
jobject ToJInteger(JNIEnv* env, s32 value) {
|
||||
return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value);
|
||||
}
|
||||
|
||||
bool GetJBoolean(JNIEnv* env, jobject jboolean) {
|
||||
return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField());
|
||||
}
|
||||
|
||||
jobject ToJBoolean(JNIEnv* env, bool value) {
|
||||
return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
|
||||
#include <jni.h>
|
||||
#include "common/common_types.h"
|
||||
|
||||
std::string GetJString(JNIEnv* env, jstring jstr);
|
||||
jstring ToJString(JNIEnv* env, std::string_view str);
|
||||
@ -13,3 +14,9 @@ jstring ToJString(JNIEnv* env, std::u16string_view str);
|
||||
|
||||
double GetJDouble(JNIEnv* env, jobject jdouble);
|
||||
jobject ToJDouble(JNIEnv* env, double value);
|
||||
|
||||
s32 GetJInteger(JNIEnv* env, jobject jinteger);
|
||||
jobject ToJInteger(JNIEnv* env, s32 value);
|
||||
|
||||
bool GetJBoolean(JNIEnv* env, jobject jboolean);
|
||||
jobject ToJBoolean(JNIEnv* env, bool value);
|
||||
|
@ -21,7 +21,7 @@ void AndroidConfig::ReloadAllValues() {
|
||||
}
|
||||
|
||||
void AndroidConfig::SaveAllValues() {
|
||||
Save();
|
||||
SaveValues();
|
||||
SaveAndroidValues();
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,7 @@ struct Values {
|
||||
Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
|
||||
Settings::Category::Overlay};
|
||||
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay};
|
||||
Settings::Setting<s32> lock_drawer{linkage, false, "lock_drawer", Settings::Category::Overlay};
|
||||
};
|
||||
|
||||
extern Values values;
|
||||
|
@ -82,7 +82,7 @@ AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobj
|
||||
const jstring string = reinterpret_cast<jstring>(env->GetObjectField(
|
||||
object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;")));
|
||||
return ResultData{GetJString(env, string),
|
||||
static_cast<Service::AM::Applets::SwkbdResult>(env->GetIntField(
|
||||
static_cast<Service::AM::Frontend::SwkbdResult>(env->GetIntField(
|
||||
object, env->GetFieldID(s_keyboard_data_class, "result", "I")))};
|
||||
}
|
||||
|
||||
@ -149,7 +149,7 @@ void AndroidKeyboard::ShowNormalKeyboard() const {
|
||||
}
|
||||
|
||||
void AndroidKeyboard::ShowTextCheckDialog(
|
||||
Service::AM::Applets::SwkbdTextCheckResult text_check_result,
|
||||
Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
|
||||
std::u16string text_check_message) const {
|
||||
LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog.");
|
||||
}
|
||||
@ -204,7 +204,7 @@ void AndroidKeyboard::InlineTextChanged(
|
||||
"\ncursor_position={}",
|
||||
Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
|
||||
|
||||
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString,
|
||||
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString,
|
||||
text_parameters.input_text, text_parameters.cursor_position);
|
||||
}
|
||||
|
||||
@ -219,7 +219,7 @@ void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
|
||||
|
||||
m_current_text += submitted_text;
|
||||
|
||||
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
|
||||
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text,
|
||||
m_current_text.size());
|
||||
}
|
||||
|
||||
@ -236,12 +236,12 @@ void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
|
||||
case KEYCODE_BACK:
|
||||
case KEYCODE_ENTER:
|
||||
m_is_inline_active = false;
|
||||
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text,
|
||||
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::DecidedEnter, m_current_text,
|
||||
static_cast<s32>(m_current_text.size()));
|
||||
break;
|
||||
case KEYCODE_DEL:
|
||||
m_current_text.pop_back();
|
||||
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text,
|
||||
submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text,
|
||||
m_current_text.size());
|
||||
break;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ public:
|
||||
|
||||
void ShowNormalKeyboard() const override;
|
||||
|
||||
void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result,
|
||||
void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
|
||||
std::u16string text_check_message) const override;
|
||||
|
||||
void ShowInlineKeyboard(
|
||||
@ -45,7 +45,7 @@ private:
|
||||
static ResultData CreateFromFrontend(jobject object);
|
||||
|
||||
std::string text;
|
||||
Service::AM::Applets::SwkbdResult result{};
|
||||
Service::AM::Frontend::SwkbdResult result{};
|
||||
};
|
||||
|
||||
void SubmitNormalText(const ResultData& result) const;
|
||||
|
@ -1,12 +1,12 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <core/core.h>
|
||||
#include <core/file_sys/mode.h>
|
||||
#include <core/file_sys/patch_manager.h>
|
||||
#include <core/loader/nro.h>
|
||||
#include <jni.h>
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/loader/nro.h"
|
||||
#include "jni.h"
|
||||
#include "jni/android_common/android_common.h"
|
||||
#include "native.h"
|
||||
|
||||
@ -79,7 +79,7 @@ extern "C" {
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobject obj,
|
||||
jstring jpath) {
|
||||
const auto file = EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
|
||||
GetJString(env, jpath), FileSys::Mode::Read);
|
||||
GetJString(env, jpath), FileSys::OpenMode::Read);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
@ -43,10 +43,27 @@ static jfieldID s_overlay_control_data_landscape_position_field;
|
||||
static jfieldID s_overlay_control_data_portrait_position_field;
|
||||
static jfieldID s_overlay_control_data_foldable_position_field;
|
||||
|
||||
static jclass s_patch_class;
|
||||
static jmethodID s_patch_constructor;
|
||||
static jfieldID s_patch_enabled_field;
|
||||
static jfieldID s_patch_name_field;
|
||||
static jfieldID s_patch_version_field;
|
||||
static jfieldID s_patch_type_field;
|
||||
static jfieldID s_patch_program_id_field;
|
||||
static jfieldID s_patch_title_id_field;
|
||||
|
||||
static jclass s_double_class;
|
||||
static jmethodID s_double_constructor;
|
||||
static jfieldID s_double_value_field;
|
||||
|
||||
static jclass s_integer_class;
|
||||
static jmethodID s_integer_constructor;
|
||||
static jfieldID s_integer_value_field;
|
||||
|
||||
static jclass s_boolean_class;
|
||||
static jmethodID s_boolean_constructor;
|
||||
static jfieldID s_boolean_value_field;
|
||||
|
||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||
|
||||
namespace IDCache {
|
||||
@ -186,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() {
|
||||
return s_overlay_control_data_foldable_position_field;
|
||||
}
|
||||
|
||||
jclass GetPatchClass() {
|
||||
return s_patch_class;
|
||||
}
|
||||
|
||||
jmethodID GetPatchConstructor() {
|
||||
return s_patch_constructor;
|
||||
}
|
||||
|
||||
jfieldID GetPatchEnabledField() {
|
||||
return s_patch_enabled_field;
|
||||
}
|
||||
|
||||
jfieldID GetPatchNameField() {
|
||||
return s_patch_name_field;
|
||||
}
|
||||
|
||||
jfieldID GetPatchVersionField() {
|
||||
return s_patch_version_field;
|
||||
}
|
||||
|
||||
jfieldID GetPatchTypeField() {
|
||||
return s_patch_type_field;
|
||||
}
|
||||
|
||||
jfieldID GetPatchProgramIdField() {
|
||||
return s_patch_program_id_field;
|
||||
}
|
||||
|
||||
jfieldID GetPatchTitleIdField() {
|
||||
return s_patch_title_id_field;
|
||||
}
|
||||
|
||||
jclass GetDoubleClass() {
|
||||
return s_double_class;
|
||||
}
|
||||
@ -198,6 +247,30 @@ jfieldID GetDoubleValueField() {
|
||||
return s_double_value_field;
|
||||
}
|
||||
|
||||
jclass GetIntegerClass() {
|
||||
return s_integer_class;
|
||||
}
|
||||
|
||||
jmethodID GetIntegerConstructor() {
|
||||
return s_integer_constructor;
|
||||
}
|
||||
|
||||
jfieldID GetIntegerValueField() {
|
||||
return s_integer_value_field;
|
||||
}
|
||||
|
||||
jclass GetBooleanClass() {
|
||||
return s_boolean_class;
|
||||
}
|
||||
|
||||
jmethodID GetBooleanConstructor() {
|
||||
return s_boolean_constructor;
|
||||
}
|
||||
|
||||
jfieldID GetBooleanValueField() {
|
||||
return s_boolean_value_field;
|
||||
}
|
||||
|
||||
} // namespace IDCache
|
||||
|
||||
#ifdef __cplusplus
|
||||
@ -278,12 +351,37 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;");
|
||||
env->DeleteLocalRef(overlay_control_data_class);
|
||||
|
||||
const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch");
|
||||
s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
|
||||
s_patch_constructor = env->GetMethodID(
|
||||
patch_class, "<init>",
|
||||
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V");
|
||||
s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
|
||||
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
|
||||
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
|
||||
s_patch_type_field = env->GetFieldID(patch_class, "type", "I");
|
||||
s_patch_program_id_field = env->GetFieldID(patch_class, "programId", "Ljava/lang/String;");
|
||||
s_patch_title_id_field = env->GetFieldID(patch_class, "titleId", "Ljava/lang/String;");
|
||||
env->DeleteLocalRef(patch_class);
|
||||
|
||||
const jclass double_class = env->FindClass("java/lang/Double");
|
||||
s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class));
|
||||
s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V");
|
||||
s_double_value_field = env->GetFieldID(double_class, "value", "D");
|
||||
env->DeleteLocalRef(double_class);
|
||||
|
||||
const jclass int_class = env->FindClass("java/lang/Integer");
|
||||
s_integer_class = reinterpret_cast<jclass>(env->NewGlobalRef(int_class));
|
||||
s_integer_constructor = env->GetMethodID(int_class, "<init>", "(I)V");
|
||||
s_integer_value_field = env->GetFieldID(int_class, "value", "I");
|
||||
env->DeleteLocalRef(int_class);
|
||||
|
||||
const jclass boolean_class = env->FindClass("java/lang/Boolean");
|
||||
s_boolean_class = reinterpret_cast<jclass>(env->NewGlobalRef(boolean_class));
|
||||
s_boolean_constructor = env->GetMethodID(boolean_class, "<init>", "(Z)V");
|
||||
s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
|
||||
env->DeleteLocalRef(boolean_class);
|
||||
|
||||
// Initialize Android Storage
|
||||
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
||||
|
||||
@ -309,7 +407,10 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||
env->DeleteGlobalRef(s_string_class);
|
||||
env->DeleteGlobalRef(s_pair_class);
|
||||
env->DeleteGlobalRef(s_overlay_control_data_class);
|
||||
env->DeleteGlobalRef(s_patch_class);
|
||||
env->DeleteGlobalRef(s_double_class);
|
||||
env->DeleteGlobalRef(s_integer_class);
|
||||
env->DeleteGlobalRef(s_boolean_class);
|
||||
|
||||
// UnInitialize applets
|
||||
SoftwareKeyboard::CleanupJNI(env);
|
||||
|
@ -43,8 +43,25 @@ jfieldID GetOverlayControlDataLandscapePositionField();
|
||||
jfieldID GetOverlayControlDataPortraitPositionField();
|
||||
jfieldID GetOverlayControlDataFoldablePositionField();
|
||||
|
||||
jclass GetPatchClass();
|
||||
jmethodID GetPatchConstructor();
|
||||
jfieldID GetPatchEnabledField();
|
||||
jfieldID GetPatchNameField();
|
||||
jfieldID GetPatchVersionField();
|
||||
jfieldID GetPatchTypeField();
|
||||
jfieldID GetPatchProgramIdField();
|
||||
jfieldID GetPatchTitleIdField();
|
||||
|
||||
jclass GetDoubleClass();
|
||||
jmethodID GetDoubleConstructor();
|
||||
jfieldID GetDoubleValueField();
|
||||
|
||||
jclass GetIntegerClass();
|
||||
jmethodID GetIntegerConstructor();
|
||||
jfieldID GetIntegerValueField();
|
||||
|
||||
jclass GetBooleanClass();
|
||||
jmethodID GetBooleanConstructor();
|
||||
jfieldID GetBooleanValueField();
|
||||
|
||||
} // namespace IDCache
|
||||
|
@ -17,6 +17,7 @@
|
||||
#include <core/file_sys/patch_manager.h>
|
||||
#include <core/file_sys/savedata_factory.h>
|
||||
#include <core/loader/nro.h>
|
||||
#include <frontend_common/content_manager.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include "common/detached_tasks.h"
|
||||
@ -34,20 +35,22 @@
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/file_sys/vfs_real.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/file_sys/vfs/vfs_real.h"
|
||||
#include "core/frontend/applets/cabinet.h"
|
||||
#include "core/frontend/applets/controller.h"
|
||||
#include "core/frontend/applets/error.h"
|
||||
#include "core/frontend/applets/general_frontend.h"
|
||||
#include "core/frontend/applets/general.h"
|
||||
#include "core/frontend/applets/mii_edit.h"
|
||||
#include "core/frontend/applets/profile_select.h"
|
||||
#include "core/frontend/applets/software_keyboard.h"
|
||||
#include "core/frontend/applets/web_browser.h"
|
||||
#include "core/hle/service/am/applet_ae.h"
|
||||
#include "core/hle/service/am/applet_manager.h"
|
||||
#include "core/hle/service/am/applet_oe.h"
|
||||
#include "core/hle/service/am/applets/applets.h"
|
||||
#include "core/hle/service/am/frontend/applets.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "frontend_common/config.h"
|
||||
@ -100,67 +103,6 @@ void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
|
||||
m_native_window = native_window;
|
||||
}
|
||||
|
||||
int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) {
|
||||
jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
|
||||
std::size_t block_size) {
|
||||
if (src == nullptr || dest == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (!dest->Resize(src->GetSize())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
using namespace Common::Literals;
|
||||
[[maybe_unused]] std::vector<u8> buffer(1_MiB);
|
||||
|
||||
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
|
||||
jconst read = src->Read(buffer.data(), buffer.size(), i);
|
||||
dest->Write(buffer.data(), read, i);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
enum InstallResult {
|
||||
Success = 0,
|
||||
SuccessFileOverwritten = 1,
|
||||
InstallError = 2,
|
||||
ErrorBaseGame = 3,
|
||||
ErrorFilenameExtension = 4,
|
||||
};
|
||||
|
||||
[[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
|
||||
if (file_extension == "nsp") {
|
||||
nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
|
||||
if (nsp->IsExtractedType()) {
|
||||
return InstallError;
|
||||
}
|
||||
} else {
|
||||
return ErrorFilenameExtension;
|
||||
}
|
||||
|
||||
if (!nsp) {
|
||||
return InstallError;
|
||||
}
|
||||
|
||||
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
|
||||
return InstallError;
|
||||
}
|
||||
|
||||
jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true,
|
||||
copy_func);
|
||||
|
||||
switch (res) {
|
||||
case FileSys::InstallResult::Success:
|
||||
return Success;
|
||||
case FileSys::InstallResult::OverwriteExisting:
|
||||
return SuccessFileOverwritten;
|
||||
case FileSys::InstallResult::ErrorBaseInstall:
|
||||
return ErrorBaseGame;
|
||||
default:
|
||||
return InstallError;
|
||||
}
|
||||
}
|
||||
|
||||
void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
|
||||
const std::string& custom_driver_dir,
|
||||
const std::string& custom_driver_name,
|
||||
@ -214,7 +156,7 @@ void EmulationSession::SurfaceChanged() {
|
||||
}
|
||||
|
||||
void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) {
|
||||
const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::Mode::Read);
|
||||
const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::OpenMode::Read);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
@ -267,6 +209,12 @@ void EmulationSession::InitializeSystem(bool reload) {
|
||||
m_system.GetFileSystemController().CreateFactories(*m_vfs);
|
||||
}
|
||||
|
||||
void EmulationSession::SetAppletId(int applet_id) {
|
||||
m_applet_id = applet_id;
|
||||
m_system.GetFrontendAppletHolder().SetCurrentAppletId(
|
||||
static_cast<Service::AM::AppletId>(m_applet_id));
|
||||
}
|
||||
|
||||
Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath) {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
|
||||
@ -281,7 +229,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
|
||||
m_system.ApplySettings();
|
||||
Settings::LogSettings();
|
||||
m_system.HIDCore().ReloadInputDevices();
|
||||
m_system.SetAppletFrontendSet({
|
||||
m_system.SetFrontendAppletSet({
|
||||
nullptr, // Amiibo Settings
|
||||
nullptr, // Controller Selector
|
||||
nullptr, // Error Display
|
||||
@ -297,7 +245,10 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
|
||||
ConfigureFilesystemProvider(filepath);
|
||||
|
||||
// Load the ROM.
|
||||
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath);
|
||||
Service::AM::FrontendAppletParameters params{
|
||||
.applet_id = static_cast<Service::AM::AppletId>(m_applet_id),
|
||||
};
|
||||
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params);
|
||||
if (m_load_result != Core::SystemResultStatus::Success) {
|
||||
return m_load_result;
|
||||
}
|
||||
@ -307,6 +258,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
|
||||
m_system.GetCpuManager().OnGpuReady();
|
||||
m_system.RegisterExitCallback([&] { HaltEmulation(); });
|
||||
|
||||
OnEmulationStarted();
|
||||
return Core::SystemResultStatus::Success;
|
||||
}
|
||||
|
||||
@ -381,6 +333,9 @@ void EmulationSession::RunEmulation() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset current applet ID.
|
||||
m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
|
||||
}
|
||||
|
||||
bool EmulationSession::IsHandheldOnly() {
|
||||
@ -410,8 +365,8 @@ void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
|
||||
jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
|
||||
|
||||
if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
|
||||
handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
|
||||
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController);
|
||||
handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
|
||||
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
|
||||
handheld->Disconnect();
|
||||
}
|
||||
}
|
||||
@ -512,10 +467,20 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject
|
||||
}
|
||||
|
||||
int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
|
||||
jstring j_file,
|
||||
jstring j_file_extension) {
|
||||
return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file),
|
||||
GetJString(env, j_file_extension));
|
||||
jstring j_file, jobject jcallback) {
|
||||
auto jlambdaClass = env->GetObjectClass(jcallback);
|
||||
auto jlambdaInvokeMethod = env->GetMethodID(
|
||||
jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
|
||||
auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
|
||||
ToJDouble(env, max), ToJDouble(env, progress));
|
||||
return GetJBoolean(env, jwasCancelled);
|
||||
};
|
||||
|
||||
return static_cast<int>(
|
||||
ContentManager::InstallNSP(EmulationSession::GetInstance().System(),
|
||||
*EmulationSession::GetInstance().System().GetFilesystem(),
|
||||
GetJString(env, j_file), callback));
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
|
||||
@ -524,8 +489,8 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* en
|
||||
u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||
std::string updatePath = GetJString(env, jupdatePath);
|
||||
std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>(
|
||||
EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath,
|
||||
FileSys::Mode::Read));
|
||||
EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
|
||||
updatePath, FileSys::OpenMode::Read));
|
||||
for (const auto& item : nsp->GetNCAs()) {
|
||||
for (const auto& nca_details : item.second) {
|
||||
if (nca_details.second->GetName().ends_with(".cnmt.nca")) {
|
||||
@ -724,6 +689,11 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass
|
||||
return ToJString(env, "JIT");
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject jobj) {
|
||||
return ToJString(env,
|
||||
EmulationSession::GetInstance().System().GPU().Renderer().GetDeviceVendor());
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
|
||||
EmulationSession::GetInstance().System().ApplySettings();
|
||||
}
|
||||
@ -763,15 +733,15 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv*
|
||||
jobject instance) {
|
||||
const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
|
||||
auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory(
|
||||
Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read);
|
||||
Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read);
|
||||
|
||||
const auto user_id = EmulationSession::GetInstance().System().GetProfileManager().GetUser(
|
||||
static_cast<std::size_t>(0));
|
||||
ASSERT(user_id);
|
||||
|
||||
const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
|
||||
EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser,
|
||||
FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0);
|
||||
{}, vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, 1,
|
||||
user_id->AsU128(), 0);
|
||||
|
||||
const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path);
|
||||
if (!Common::FS::CreateParentDirs(full_path)) {
|
||||
@ -798,13 +768,12 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, j
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentAppletId(JNIEnv* env, jclass clazz,
|
||||
jint jappletId) {
|
||||
EmulationSession::GetInstance().System().GetAppletManager().SetCurrentAppletId(
|
||||
static_cast<Service::AM::Applets::AppletId>(jappletId));
|
||||
EmulationSession::GetInstance().SetAppletId(jappletId);
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclass clazz,
|
||||
jint jcabinetMode) {
|
||||
EmulationSession::GetInstance().System().GetAppletManager().SetCabinetMode(
|
||||
EmulationSession::GetInstance().System().GetFrontendAppletHolder().SetCabinetMode(
|
||||
static_cast<Service::NFP::CabinetMode>(jcabinetMode));
|
||||
}
|
||||
|
||||
@ -824,9 +793,9 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
|
||||
return true;
|
||||
}
|
||||
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
|
||||
jstring jpath,
|
||||
jstring jprogramId) {
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
|
||||
jstring jpath,
|
||||
jstring jprogramId) {
|
||||
const auto path = GetJString(env, jpath);
|
||||
const auto vFile =
|
||||
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
|
||||
@ -843,20 +812,78 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env,
|
||||
FileSys::VirtualFile update_raw;
|
||||
loader->ReadUpdateRaw(update_raw);
|
||||
|
||||
auto addons = pm.GetPatchVersionNames(update_raw);
|
||||
auto jemptyString = ToJString(env, "");
|
||||
auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
|
||||
jemptyString, jemptyString);
|
||||
jobjectArray jaddonsArray =
|
||||
env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
|
||||
auto patches = pm.GetPatches(update_raw);
|
||||
jobjectArray jpatchArray =
|
||||
env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr);
|
||||
int i = 0;
|
||||
for (const auto& addon : addons) {
|
||||
jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
|
||||
ToJString(env, addon.first), ToJString(env, addon.second));
|
||||
env->SetObjectArrayElement(jaddonsArray, i, jaddon);
|
||||
for (const auto& patch : patches) {
|
||||
jobject jpatch = env->NewObject(
|
||||
IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled,
|
||||
ToJString(env, patch.name), ToJString(env, patch.version),
|
||||
static_cast<jint>(patch.type), ToJString(env, std::to_string(patch.program_id)),
|
||||
ToJString(env, std::to_string(patch.title_id)));
|
||||
env->SetObjectArrayElement(jpatchArray, i, jpatch);
|
||||
++i;
|
||||
}
|
||||
return jaddonsArray;
|
||||
return jpatchArray;
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj,
|
||||
jstring jprogramId) {
|
||||
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||
ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(),
|
||||
program_id);
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj,
|
||||
jstring jprogramId) {
|
||||
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||
ContentManager::RemoveAllDLC(EmulationSession::GetInstance().System(), program_id);
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId,
|
||||
jstring jname) {
|
||||
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||
ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(),
|
||||
program_id, GetJString(env, jname));
|
||||
}
|
||||
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyInstalledContents(JNIEnv* env,
|
||||
jobject jobj,
|
||||
jobject jcallback) {
|
||||
auto jlambdaClass = env->GetObjectClass(jcallback);
|
||||
auto jlambdaInvokeMethod = env->GetMethodID(
|
||||
jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
|
||||
auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
|
||||
ToJDouble(env, max), ToJDouble(env, progress));
|
||||
return GetJBoolean(env, jwasCancelled);
|
||||
};
|
||||
|
||||
auto& session = EmulationSession::GetInstance();
|
||||
std::vector<std::string> result = ContentManager::VerifyInstalledContents(
|
||||
session.System(), *session.GetContentProvider(), callback);
|
||||
jobjectArray jresult =
|
||||
env->NewObjectArray(result.size(), IDCache::GetStringClass(), ToJString(env, ""));
|
||||
for (size_t i = 0; i < result.size(); ++i) {
|
||||
env->SetObjectArrayElement(jresult, i, ToJString(env, result[i]));
|
||||
}
|
||||
return jresult;
|
||||
}
|
||||
|
||||
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyGameContents(JNIEnv* env, jobject jobj,
|
||||
jstring jpath, jobject jcallback) {
|
||||
auto jlambdaClass = env->GetObjectClass(jcallback);
|
||||
auto jlambdaInvokeMethod = env->GetMethodID(
|
||||
jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
|
||||
auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
|
||||
ToJDouble(env, max), ToJDouble(env, progress));
|
||||
return GetJBoolean(env, jwasCancelled);
|
||||
};
|
||||
auto& session = EmulationSession::GetInstance();
|
||||
return static_cast<jint>(
|
||||
ContentManager::VerifyGameContents(session.System(), GetJString(env, jpath), callback));
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
|
||||
@ -875,10 +902,10 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j
|
||||
|
||||
const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
|
||||
auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir),
|
||||
FileSys::Mode::Read);
|
||||
FileSys::OpenMode::Read);
|
||||
|
||||
const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
|
||||
system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
|
||||
{}, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
|
||||
program_id, user_id->AsU128(), 0);
|
||||
return ToJString(env, user_save_data_path);
|
||||
}
|
||||
@ -905,4 +932,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env,
|
||||
EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobject jobj) {
|
||||
auto& system = EmulationSession::GetInstance().System();
|
||||
system.GetFileSystemController().CreateFactories(*system.GetFilesystem());
|
||||
return ContentManager::AreKeysPresent();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/hle/service/acc/profile_manager.h"
|
||||
#include "core/perf_stats.h"
|
||||
#include "frontend_common/content_manager.h"
|
||||
#include "jni/applets/software_keyboard.h"
|
||||
#include "jni/emu_window/emu_window.h"
|
||||
#include "video_core/rasterizer_interface.h"
|
||||
@ -29,7 +30,6 @@ public:
|
||||
void SetNativeWindow(ANativeWindow* native_window);
|
||||
void SurfaceChanged();
|
||||
|
||||
int InstallFileToNand(std::string filename, std::string file_extension);
|
||||
void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
|
||||
const std::string& custom_driver_name,
|
||||
const std::string& file_redirect_dir);
|
||||
@ -45,6 +45,7 @@ public:
|
||||
const Core::PerfStatsResults& PerfStats();
|
||||
void ConfigureFilesystemProvider(const std::string& filepath);
|
||||
void InitializeSystem(bool reload);
|
||||
void SetAppletId(int applet_id);
|
||||
Core::SystemResultStatus InitializeEmulation(const std::string& filepath);
|
||||
|
||||
bool IsHandheldOnly();
|
||||
@ -77,6 +78,7 @@ private:
|
||||
std::atomic<bool> m_is_paused = false;
|
||||
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
|
||||
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
|
||||
int m_applet_id{1};
|
||||
|
||||
// GPU driver parameters
|
||||
std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
|
||||
|
@ -205,7 +205,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEn
|
||||
jstring jkey) {
|
||||
auto setting = getSetting<std::string>(env, jkey);
|
||||
if (setting != nullptr) {
|
||||
return setting->RuntimeModfiable();
|
||||
return setting->RuntimeModifiable();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
9
src/android/app/src/main/res/drawable/ic_lock.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_lock.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
|
||||
</vector>
|
9
src/android/app/src/main/res/drawable/ic_shortcut.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_shortcut.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M280,920q-33,0 -56.5,-23.5T200,840v-720q0,-33 23.5,-56.5T280,40h400q33,0 56.5,23.5T760,120v160h-80v-40L280,240v480h400v-40h80v160q0,33 -23.5,56.5T680,920L280,920ZM686,520L480,520v120h-80v-120q0,-33 23.5,-56.5T480,440h206l-62,-64 56,-56 160,160 -160,160 -56,-56 62,-64Z" />
|
||||
</vector>
|
@ -11,12 +11,14 @@
|
||||
android:id="@+id/appbar_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/about" />
|
||||
|
||||
@ -28,6 +30,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
@ -147,7 +150,7 @@
|
||||
android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_build_hash"
|
||||
android:id="@+id/button_version_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
@ -164,7 +167,7 @@
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_build_hash"
|
||||
android:id="@+id/text_version_name"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -0,0 +1,155 @@
|
||||
<?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:id="@+id/coordinator_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroll_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/content_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="3"
|
||||
android:gravity="top|center_horizontal"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_copy"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/copy_details" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_verify_integrity"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/verify_integrity" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="1">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/path_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:editable="false"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="none"
|
||||
android:minHeight="48dp"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="1.0.0" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/program_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/program_id_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:editable="false"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="none"
|
||||
android:minHeight="48dp"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="1.0.0" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/developer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/developer_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:editable="false"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="none"
|
||||
android:minHeight="48dp"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="1.0.0" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/version"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/version_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:editable="false"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="none"
|
||||
android:minHeight="48dp"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="1.0.0" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -14,6 +14,7 @@
|
||||
android:clipToPadding="false"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/icon_layout"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
@ -43,16 +44,35 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_margin="8dp"
|
||||
app:icon="@drawable/ic_back"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="?attr/colorOnSurface" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_back"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_shortcut"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_shortcut"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
|
@ -23,6 +23,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:focusable="false"
|
||||
android:clickable="false"
|
||||
android:checked="false" />
|
||||
|
||||
|
@ -6,16 +6,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:focusable="true">
|
||||
android:layout_marginVertical="12dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:animateLayoutChanges="true">
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/path"
|
||||
|
@ -2,16 +2,16 @@
|
||||
<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"
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:id="@+id/option_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:backgroundTint="?attr/colorSurfaceVariant"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
android:focusable="true"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/option_layout"
|
||||
|
@ -1,8 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="24dp"
|
||||
app:trackCornerRadius="4dp" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/message"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="24dp"
|
||||
app:trackCornerRadius="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -11,12 +11,14 @@
|
||||
android:id="@+id/appbar_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:title="@string/about"
|
||||
app:navigationIcon="@drawable/ic_back" />
|
||||
|
||||
@ -28,6 +30,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
android:fadeScrollbars="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
@ -148,7 +151,7 @@
|
||||
android:layout_marginHorizontal="20dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_build_hash"
|
||||
android:id="@+id/button_version_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="16dp"
|
||||
@ -165,7 +168,7 @@
|
||||
android:text="@string/build" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text_build_hash"
|
||||
android:id="@+id/text_version_name"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -11,6 +11,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
@ -19,6 +20,7 @@
|
||||
android:id="@+id/toolbar_addons"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
@ -28,6 +30,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:nextFocusDown="@id/button_install"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -10,12 +10,14 @@
|
||||
android:id="@+id/appbar_applets"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_applets"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/applets" />
|
||||
|
||||
|
@ -15,12 +15,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:liftOnScrollTargetViewId="@id/list_drivers">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_drivers"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/gpu_driver_manager" />
|
||||
|
||||
|
@ -11,12 +11,14 @@
|
||||
android:id="@+id/appbar_ea"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/early_access" />
|
||||
|
||||
@ -30,6 +32,7 @@
|
||||
android:paddingBottom="20dp"
|
||||
android:scrollbars="vertical"
|
||||
android:fadeScrollbars="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
|
@ -5,6 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
tools:context="org.yuzu.yuzu_emu.fragments.EmulationFragment"
|
||||
tools:openDrawer="start">
|
||||
|
||||
@ -24,7 +25,8 @@
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false" />
|
||||
android:focusableInTouchMode="false"
|
||||
android:defaultFocusHighlightEnabled="false" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/loading_indicator"
|
||||
@ -32,7 +34,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:clickable="false">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
@ -118,6 +120,7 @@
|
||||
android:layout_gravity="center"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<Button
|
||||
@ -160,6 +163,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:focusedByDefault="true"
|
||||
app:headerLayout="@layout/header_in_game"
|
||||
app:menu="@menu/menu_in_game"
|
||||
tools:visibility="gone" />
|
||||
|
@ -15,12 +15,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:liftOnScrollTargetViewId="@id/list_folders">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_folders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/game_folders" />
|
||||
|
||||
@ -31,6 +33,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@ -11,12 +11,14 @@
|
||||
android:id="@+id/appbar_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
@ -25,6 +27,7 @@
|
||||
android:id="@+id/scroll_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<LinearLayout
|
||||
@ -118,6 +121,14 @@
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/copy_details" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_verify_integrity"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/verify_integrity" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
@ -13,7 +12,8 @@
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
android:fadeScrollbars="false"
|
||||
android:clipToPadding="false">
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_all"
|
||||
@ -22,16 +22,35 @@
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_gravity="start"
|
||||
app:icon="@drawable/ic_back"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="?attr/colorOnSurface" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_back"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_back"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_shortcut"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_shortcut"
|
||||
app:iconSize="24dp"
|
||||
app:iconTint="?attr/colorOnSurface"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
@ -45,7 +64,7 @@
|
||||
android:id="@+id/image_game_screen"
|
||||
android:layout_width="175dp"
|
||||
android:layout_height="175dp"
|
||||
tools:src="@drawable/default_icon"/>
|
||||
tools:src="@drawable/default_icon" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@ -68,7 +87,7 @@
|
||||
android:id="@+id/list_properties"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:listitem="@layout/card_simple_outlined" />
|
||||
android:defaultFocusHighlightEnabled="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -27,6 +27,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
tools:listitem="@layout/card_game" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -7,7 +7,8 @@
|
||||
android:background="?attr/colorSurface"
|
||||
android:scrollbars="vertical"
|
||||
android:fadeScrollbars="false"
|
||||
android:clipToPadding="false">
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/linear_layout_settings"
|
||||
|
@ -10,12 +10,14 @@
|
||||
android:id="@+id/appbar_installables"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_installables"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:title="@string/manage_yuzu_data"
|
||||
app:navigationIcon="@drawable/ic_back" />
|
||||
|
||||
|
@ -10,12 +10,14 @@
|
||||
android:id="@+id/appbar_licenses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_licenses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:title="@string/licenses"
|
||||
app:navigationIcon="@drawable/ic_back" />
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:elevation="0dp">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
@ -24,6 +25,7 @@
|
||||
android:id="@+id/toolbar_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_back" />
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:focusable="false"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="16dp">
|
||||
|
||||
@ -14,12 +14,11 @@
|
||||
android:id="@+id/text_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/addon_switch"
|
||||
app:layout_constraintEnd_toStartOf="@+id/addon_switch"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/addon_checkbox"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/addon_switch">
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
@ -42,16 +41,29 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/addon_switch"
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/addon_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusLeft="@id/addon_container"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintTop_toTopOf="@+id/text_container"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/text_container"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_delete" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_delete"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="@string/delete"
|
||||
android:tooltipText="@string/delete"
|
||||
app:icon="@drawable/ic_delete"
|
||||
app:iconTint="?attr/colorControlNormal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/text_container"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="@+id/addon_checkbox"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -69,7 +69,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
android:text="@string/clear"
|
||||
android:text="@string/use_global_setting"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -63,7 +63,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/clear"
|
||||
android:text="@string/use_global_setting"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
@ -12,4 +12,5 @@
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textStyle="bold"
|
||||
android:focusable="false"
|
||||
tools:text="CPU Settings" />
|
||||
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_driver_use_global"
|
||||
android:title="@string/use_global_setting" />
|
||||
|
||||
</menu>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user