Compare commits
210 Commits
android-14
...
android-17
Author | SHA1 | Date | |
---|---|---|---|
7645aa3839 | |||
6d509702bf | |||
0fb1090e30 | |||
93f4696e2a | |||
4d2090a9a9 | |||
c8f0c34202 | |||
09bfc852dc | |||
ace74bd066 | |||
f6ee53af14 | |||
6c6cb5745f | |||
3262c0f747 | |||
9323a1f9b2 | |||
f02a8d0ae9 | |||
8517d7cb44 | |||
cb4b4f3d6e | |||
21e7f86697 | |||
347b3bd18d | |||
755c45777f | |||
d677052e8c | |||
95bfc542aa | |||
d0c60605ab | |||
6697b665ca | |||
12178c694a | |||
de1e5584b3 | |||
1559984f77 | |||
467ac4fdfe | |||
69b7100dac | |||
14dc41d4b3 | |||
ad049f13aa | |||
20e0407235 | |||
4f569fd568 | |||
553dac2ae0 | |||
96abe0d7d3 | |||
47e44a6693 | |||
cf8c7d4ed3 | |||
5165ed9efd | |||
05e3db3ac9 | |||
e3491a9ee8 | |||
6a1ddc5028 | |||
b1d4804c07 | |||
c57ae803a6 | |||
db7b2bc8f1 | |||
31bf57a310 | |||
cae675343c | |||
35501ba41c | |||
419055e484 | |||
91290b9be4 | |||
820f113d9e | |||
373a1ff2ce | |||
4d6b6ba76c | |||
4aa713e861 | |||
9e9aed41be | |||
3d268b8480 | |||
ad7445d4cc | |||
3a30271219 | |||
bb5196aaae | |||
d3070cafa7 | |||
5cd3b6f58c | |||
bedc758fe7 | |||
76701185ad | |||
f1cb14eb54 | |||
f4f4a469a9 | |||
9e5b4052ed | |||
234867b84d | |||
61e8c5f798 | |||
4b60aec190 | |||
bbc0ed118d | |||
ecfba79d98 | |||
310834aea2 | |||
6a1fa9bb17 | |||
db8a601cf8 | |||
1bb76201e6 | |||
372bca5945 | |||
93c19a40bf | |||
d0a75580da | |||
345ec25532 | |||
a94721fde0 | |||
816c7a8d1f | |||
efe52db690 | |||
d61df0f400 | |||
b14547b8b6 | |||
97ad3e7530 | |||
0589a32f75 | |||
617dc0f822 | |||
fcfa8b680b | |||
94244437de | |||
53956a2990 | |||
a7731abb72 | |||
50fd029eaa | |||
a2b567dfd6 | |||
b770f6a985 | |||
797e8fdbc3 | |||
b8c5027686 | |||
65e646eeba | |||
fba3fa705d | |||
09e8fb75ce | |||
6ca530a721 | |||
e01c535178 | |||
7239547ead | |||
7fc06260d1 | |||
e357896674 | |||
225f4f40cb | |||
927be75616 | |||
00965e6c34 | |||
4bf1f217ae | |||
fcc85abe27 | |||
6851e93296 | |||
67660972c9 | |||
ffbba74c91 | |||
2b0cf73bf0 | |||
a093f3d47a | |||
4f600f746a | |||
360418f1a1 | |||
3bc7575c47 | |||
fde8dc1652 | |||
b8f83aa4bf | |||
85b1e17df6 | |||
4144c517a5 | |||
8ad5f2c506 | |||
2a3f84aaf2 | |||
030e6b3980 | |||
e8ad603cd9 | |||
b560ade663 | |||
d10464de30 | |||
64f68e9635 | |||
462ba1b360 | |||
4a86a55174 | |||
86d26914a2 | |||
6ae4177b25 | |||
f6bf8b3ed3 | |||
345fb6b226 | |||
87a9dc9489 | |||
6c6e8b8de0 | |||
5acffe75df | |||
ac222ceba2 | |||
f9d4827102 | |||
7ea7c72dde | |||
809230f634 | |||
698c854d5b | |||
ca5b135ddf | |||
dbddc627d4 | |||
62fc386bb4 | |||
f2eb3c579f | |||
2fce812026 | |||
e975f3cde9 | |||
6b5fb2063f | |||
70c3d36536 | |||
d590cfb9d0 | |||
ded419ef2b | |||
4c3f898789 | |||
46c259bb20 | |||
adc3079613 | |||
15bebf1695 | |||
5c840334b8 | |||
a05c242429 | |||
bd59934350 | |||
11b123ba01 | |||
24e7ace876 | |||
62586c1676 | |||
108737fcc6 | |||
abfebe5cc4 | |||
a22a025c5b | |||
a529ef4c09 | |||
875568bb3e | |||
988e557ec8 | |||
6d2af32f29 | |||
8f9d5c3143 | |||
dc0fb56f3a | |||
7ba4a8f4a3 | |||
8ef1fdafa2 | |||
d597383ab2 | |||
5feda37688 | |||
34e4012998 | |||
c1924951ad | |||
5646e313a0 | |||
f447996080 | |||
42b34a0dc5 | |||
fe5e4bd846 | |||
a53cd2854e | |||
1d731dd1ff | |||
8225ac004e | |||
52e6b8a2d3 | |||
13131e602f | |||
7761f29892 | |||
e92b10f971 | |||
9268f265a1 | |||
e445ef9d60 | |||
40bb176c39 | |||
8a79dd2d6c | |||
d5de9402ee | |||
4cd3f9f4f9 | |||
167efb2d2b | |||
8e0e066c3f | |||
f0ee3e29cb | |||
5d4da07943 | |||
45c87c7e6e | |||
90e87c40e8 | |||
6b7dc587cf | |||
f05cb69d4f | |||
382cf087a0 | |||
0751488727 | |||
4bc932261b | |||
5fb1a83e4c | |||
6da8301773 | |||
fedeff7a89 | |||
9de99839bd | |||
45b6161582 | |||
e7dd968ac4 | |||
db1d32485e | |||
a595ed499d |
@ -6,7 +6,12 @@
|
|||||||
export NDK_CCACHE="$(which ccache)"
|
export NDK_CCACHE="$(which ccache)"
|
||||||
ccache -s
|
ccache -s
|
||||||
|
|
||||||
BUILD_FLAVOR=mainline
|
BUILD_FLAVOR="mainline"
|
||||||
|
|
||||||
|
BUILD_TYPE="release"
|
||||||
|
if [ "${GITHUB_REPOSITORY}" == "yuzu-emu/yuzu" ]; then
|
||||||
|
BUILD_TYPE="relWithDebInfo"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! -z "${ANDROID_KEYSTORE_B64}" ]; then
|
if [ ! -z "${ANDROID_KEYSTORE_B64}" ]; then
|
||||||
export ANDROID_KEYSTORE_FILE="${GITHUB_WORKSPACE}/ks.jks"
|
export ANDROID_KEYSTORE_FILE="${GITHUB_WORKSPACE}/ks.jks"
|
||||||
@ -15,7 +20,7 @@ fi
|
|||||||
|
|
||||||
cd src/android
|
cd src/android
|
||||||
chmod +x ./gradlew
|
chmod +x ./gradlew
|
||||||
./gradlew "assemble${BUILD_FLAVOR}Release" "bundle${BUILD_FLAVOR}Release"
|
./gradlew "assemble${BUILD_FLAVOR}${BUILD_TYPE}" "bundle${BUILD_FLAVOR}${BUILD_TYPE}"
|
||||||
|
|
||||||
ccache -s
|
ccache -s
|
||||||
|
|
||||||
|
@ -7,9 +7,16 @@
|
|||||||
|
|
||||||
REV_NAME="yuzu-${GITDATE}-${GITREV}"
|
REV_NAME="yuzu-${GITDATE}-${GITREV}"
|
||||||
|
|
||||||
BUILD_FLAVOR=mainline
|
BUILD_FLAVOR="mainline"
|
||||||
|
|
||||||
cp src/android/app/build/outputs/apk/"${BUILD_FLAVOR}/release/app-${BUILD_FLAVOR}-release.apk" \
|
BUILD_TYPE_LOWER="release"
|
||||||
|
BUILD_TYPE_UPPER="Release"
|
||||||
|
if [ "${GITHUB_REPOSITORY}" == "yuzu-emu/yuzu" ]; then
|
||||||
|
BUILD_TYPE_LOWER="relWithDebInfo"
|
||||||
|
BUILD_TYPE_UPPER="RelWithDebInfo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp src/android/app/build/outputs/apk/"${BUILD_FLAVOR}/${BUILD_TYPE_LOWER}/app-${BUILD_FLAVOR}-${BUILD_TYPE_LOWER}.apk" \
|
||||||
"artifacts/${REV_NAME}.apk"
|
"artifacts/${REV_NAME}.apk"
|
||||||
cp src/android/app/build/outputs/bundle/"${BUILD_FLAVOR}Release"/"app-${BUILD_FLAVOR}-release.aab" \
|
cp src/android/app/build/outputs/bundle/"${BUILD_FLAVOR}${BUILD_TYPE_UPPER}"/"app-${BUILD_FLAVOR}-${BUILD_TYPE_LOWER}.aab" \
|
||||||
"artifacts/${REV_NAME}.aab"
|
"artifacts/${REV_NAME}.aab"
|
||||||
|
@ -3,4 +3,4 @@
|
|||||||
|
|
||||||
[codespell]
|
[codespell]
|
||||||
skip = ./.git,./build,./dist,./Doxyfile,./externals,./LICENSES,./src/android/app/src/main/res
|
skip = ./.git,./build,./dist,./Doxyfile,./externals,./LICENSES,./src/android/app/src/main/res
|
||||||
ignore-words-list = aci,allright,ba,canonicalizations,deques,froms,hda,inout,lod,masia,nam,nax,nce,nd,optin,pullrequests,pullrequest,te,transfered,unstall,uscaled,vas,zink
|
ignore-words-list = aci,allright,ba,canonicalizations,deques,fpr,froms,hda,inout,lod,masia,nam,nax,nce,nd,optin,pullrequests,pullrequest,te,transfered,unstall,uscaled,vas,zink
|
||||||
|
3
.github/workflows/verify.yml
vendored
3
.github/workflows/verify.yml
vendored
@ -79,7 +79,8 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
brew install autoconf automake boost@1.83 ccache ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@5 sdl2 speexdsp zlib zlib zstd
|
# workaround for https://github.com/actions/setup-python/issues/577
|
||||||
|
brew install autoconf automake boost@1.83 ccache ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@5 sdl2 speexdsp zlib zlib zstd || brew link --overwrite python@3.12
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
mkdir build
|
mkdir build
|
||||||
|
@ -142,6 +142,9 @@ if (YUZU_USE_BUNDLED_VCPKG)
|
|||||||
if (ENABLE_WEB_SERVICE)
|
if (ENABLE_WEB_SERVICE)
|
||||||
list(APPEND VCPKG_MANIFEST_FEATURES "web-service")
|
list(APPEND VCPKG_MANIFEST_FEATURES "web-service")
|
||||||
endif()
|
endif()
|
||||||
|
if (ANDROID)
|
||||||
|
list(APPEND VCPKG_MANIFEST_FEATURES "android")
|
||||||
|
endif()
|
||||||
|
|
||||||
include(${CMAKE_SOURCE_DIR}/externals/vcpkg/scripts/buildsystems/vcpkg.cmake)
|
include(${CMAKE_SOURCE_DIR}/externals/vcpkg/scripts/buildsystems/vcpkg.cmake)
|
||||||
elseif(NOT "$ENV{VCPKG_TOOLCHAIN_FILE}" STREQUAL "")
|
elseif(NOT "$ENV{VCPKG_TOOLCHAIN_FILE}" STREQUAL "")
|
||||||
@ -302,7 +305,7 @@ find_package(ZLIB 1.2 REQUIRED)
|
|||||||
find_package(zstd 1.5 REQUIRED)
|
find_package(zstd 1.5 REQUIRED)
|
||||||
|
|
||||||
if (NOT YUZU_USE_EXTERNAL_VULKAN_HEADERS)
|
if (NOT YUZU_USE_EXTERNAL_VULKAN_HEADERS)
|
||||||
find_package(Vulkan 1.3.256 REQUIRED)
|
find_package(Vulkan 1.3.274 REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (ENABLE_LIBUSB)
|
if (ENABLE_LIBUSB)
|
||||||
|
13
README.md
13
README.md
@ -1,3 +1,16 @@
|
|||||||
|
| Pull Request | Commit | Title | Author | Merged? |
|
||||||
|
|----|----|----|----|----|
|
||||||
|
| [12454](https://github.com/yuzu-emu/yuzu//pull/12454) | [`3a4e7d45f`](https://github.com/yuzu-emu/yuzu//pull/12454/files) | core_timing: minor refactors | [liamwhite](https://github.com/liamwhite/) | Yes |
|
||||||
|
| [12466](https://github.com/yuzu-emu/yuzu//pull/12466) | [`adb2af0a2`](https://github.com/yuzu-emu/yuzu//pull/12466/files) | core: track separate heap allocation for linux | [liamwhite](https://github.com/liamwhite/) | Yes |
|
||||||
|
| [12501](https://github.com/yuzu-emu/yuzu//pull/12501) | [`d1c99c5d5`](https://github.com/yuzu-emu/yuzu//pull/12501/files) | ips_layer: prevent out of bounds access with offset exceeding module size | [liamwhite](https://github.com/liamwhite/) | Yes |
|
||||||
|
| [12513](https://github.com/yuzu-emu/yuzu//pull/12513) | [`558192abf`](https://github.com/yuzu-emu/yuzu//pull/12513/files) | jit: use code memory handles correctly | [liamwhite](https://github.com/liamwhite/) | Yes |
|
||||||
|
| [12518](https://github.com/yuzu-emu/yuzu//pull/12518) | [`aa4d15594`](https://github.com/yuzu-emu/yuzu//pull/12518/files) | android: Migrate remaining settings to ini | [t895](https://github.com/t895/) | Yes |
|
||||||
|
|
||||||
|
|
||||||
|
End of merge log. You can find the original README.md below the break.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
19
dist/72-yuzu-input.rules
vendored
Normal file
19
dist/72-yuzu-input.rules
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
# Allow systemd-logind to manage user access to hidraw with this file
|
||||||
|
# On most systems, this file should be installed to /etc/udev/rules.d/72-yuzu-input.rules
|
||||||
|
# Consult your distro if this is not the case
|
||||||
|
|
||||||
|
# Switch Pro Controller (USB/Bluetooth)
|
||||||
|
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
|
||||||
|
KERNEL=="hidraw*", KERNELS=="*057e:2009*", MODE="0660", TAG+="uaccess"
|
||||||
|
|
||||||
|
# Joy-Con L (Bluetooth)
|
||||||
|
KERNEL=="hidraw*", KERNELS=="*057e:2006*", MODE="0660", TAG+="uaccess"
|
||||||
|
|
||||||
|
# Joy-Con R (Bluetooth)
|
||||||
|
KERNEL=="hidraw*", KERNELS=="*057e:2007*", MODE="0660", TAG+="uaccess"
|
||||||
|
|
||||||
|
# Joy-Con Charging Grip (USB)
|
||||||
|
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"
|
2
externals/Vulkan-Headers
vendored
2
externals/Vulkan-Headers
vendored
Submodule externals/Vulkan-Headers updated: df60f03168...80207f9da8
2
externals/vcpkg
vendored
2
externals/vcpkg
vendored
Submodule externals/vcpkg updated: ef2eef1734...a42af01b72
@ -10,7 +10,7 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.8.21"
|
kotlin("plugin.serialization") version "1.9.20"
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
|
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
|
||||||
}
|
}
|
||||||
@ -174,7 +174,8 @@ android {
|
|||||||
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
|
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
|
||||||
"-DYUZU_USE_BUNDLED_VCPKG=ON",
|
"-DYUZU_USE_BUNDLED_VCPKG=ON",
|
||||||
"-DYUZU_USE_BUNDLED_FFMPEG=ON",
|
"-DYUZU_USE_BUNDLED_FFMPEG=ON",
|
||||||
"-DYUZU_ENABLE_LTO=ON"
|
"-DYUZU_ENABLE_LTO=ON",
|
||||||
|
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
|
||||||
)
|
)
|
||||||
|
|
||||||
abiFilters("arm64-v8a", "x86_64")
|
abiFilters("arm64-v8a", "x86_64")
|
||||||
|
@ -230,8 +230,6 @@ object NativeLibrary {
|
|||||||
*/
|
*/
|
||||||
external fun onTouchReleased(finger_id: Int)
|
external fun onTouchReleased(finger_id: Int)
|
||||||
|
|
||||||
external fun initGameIni(gameID: String?)
|
|
||||||
|
|
||||||
external fun setAppDirectory(directory: String)
|
external fun setAppDirectory(directory: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -241,6 +239,8 @@ object NativeLibrary {
|
|||||||
*/
|
*/
|
||||||
external fun installFileToNand(filename: String, extension: String): Int
|
external fun installFileToNand(filename: String, extension: String): Int
|
||||||
|
|
||||||
|
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
|
||||||
|
|
||||||
external fun initializeGpuDriver(
|
external fun initializeGpuDriver(
|
||||||
hookLibDir: String?,
|
hookLibDir: String?,
|
||||||
customDriverDir: String?,
|
customDriverDir: String?,
|
||||||
@ -252,18 +252,11 @@ object NativeLibrary {
|
|||||||
|
|
||||||
external fun initializeSystem(reload: Boolean)
|
external fun initializeSystem(reload: Boolean)
|
||||||
|
|
||||||
external fun defaultCPUCore(): Int
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begins emulation.
|
* Begins emulation.
|
||||||
*/
|
*/
|
||||||
external fun run(path: String?)
|
external fun run(path: String?)
|
||||||
|
|
||||||
/**
|
|
||||||
* Begins emulation from the specified savestate.
|
|
||||||
*/
|
|
||||||
external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
|
|
||||||
|
|
||||||
// Surface Handling
|
// Surface Handling
|
||||||
external fun surfaceChanged(surf: Surface?)
|
external fun surfaceChanged(surf: Surface?)
|
||||||
|
|
||||||
@ -304,10 +297,9 @@ object NativeLibrary {
|
|||||||
*/
|
*/
|
||||||
external fun getCpuBackend(): String
|
external fun getCpuBackend(): String
|
||||||
|
|
||||||
/**
|
external fun applySettings()
|
||||||
* Notifies the core emulation that the orientation has changed.
|
|
||||||
*/
|
external fun logSettings()
|
||||||
external fun notifyOrientationChange(layout_option: Int, rotation: Int)
|
|
||||||
|
|
||||||
enum class CoreError {
|
enum class CoreError {
|
||||||
ErrorSystemFiles,
|
ErrorSystemFiles,
|
||||||
@ -538,6 +530,35 @@ object NativeLibrary {
|
|||||||
*/
|
*/
|
||||||
external fun isFirmwareAvailable(): Boolean
|
external fun isFirmwareAvailable(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the PatchManager for any addons that are available
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the save location for a specific game
|
||||||
|
*
|
||||||
|
* @param programId String representation of a game's program ID
|
||||||
|
* @return Save data path that may not exist yet
|
||||||
|
*/
|
||||||
|
external fun getSavePath(programId: String): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a file to the manual filesystem provider in our EmulationSession instance
|
||||||
|
* @param path Path to the file we're adding. Can be a string representation of a [Uri] or
|
||||||
|
* a normal path
|
||||||
|
*/
|
||||||
|
external fun addFileToFilesystemProvider(path: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all files added to the manual filesystem provider in our EmulationSession instance
|
||||||
|
*/
|
||||||
|
external fun clearFilesystemProvider()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button type for use in onTouchEvent
|
* Button type for use in onTouchEvent
|
||||||
*/
|
*/
|
||||||
|
@ -49,6 +49,7 @@ import org.yuzu.yuzu_emu.utils.ForegroundService
|
|||||||
import org.yuzu.yuzu_emu.utils.InputHandler
|
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||||
import org.yuzu.yuzu_emu.utils.Log
|
import org.yuzu.yuzu_emu.utils.Log
|
||||||
import org.yuzu.yuzu_emu.utils.MemoryUtil
|
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.NfcReader
|
||||||
import org.yuzu.yuzu_emu.utils.ThemeHelper
|
import org.yuzu.yuzu_emu.utils.ThemeHelper
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
@ -170,9 +171,14 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||||||
stopMotionSensorListener()
|
stopMotionSensorListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onUserLeaveHint() {
|
override fun onUserLeaveHint() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
|
if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
|
||||||
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
|
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
|
||||||
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
|
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
|
||||||
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
|
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
|
||||||
@ -284,7 +290,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||||||
|
|
||||||
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
|
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
|
||||||
PictureInPictureParams.Builder {
|
PictureInPictureParams.Builder {
|
||||||
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
|
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
|
||||||
0 -> Rational(16, 9)
|
0 -> Rational(16, 9)
|
||||||
1 -> Rational(4, 3)
|
1 -> Rational(4, 3)
|
||||||
2 -> Rational(21, 9)
|
2 -> Rational(21, 9)
|
||||||
@ -331,7 +337,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||||||
pictureInPictureActions.add(pauseRemoteAction)
|
pictureInPictureActions.add(pauseRemoteAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BooleanSetting.AUDIO_MUTED.boolean) {
|
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||||
val unmuteIcon = Icon.createWithResource(
|
val unmuteIcon = Icon.createWithResource(
|
||||||
this@EmulationActivity,
|
this@EmulationActivity,
|
||||||
R.drawable.ic_pip_unmute
|
R.drawable.ic_pip_unmute
|
||||||
@ -376,7 +382,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||||||
val isEmulationActive = emulationViewModel.emulationStarted.value &&
|
val isEmulationActive = emulationViewModel.emulationStarted.value &&
|
||||||
!emulationViewModel.isEmulationStopping.value
|
!emulationViewModel.isEmulationStopping.value
|
||||||
pictureInPictureParamsBuilder.setAutoEnterEnabled(
|
pictureInPictureParamsBuilder.setAutoEnterEnabled(
|
||||||
BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive
|
BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
|
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
|
||||||
@ -390,9 +396,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||||||
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
|
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
|
||||||
}
|
}
|
||||||
if (intent.action == actionUnmute) {
|
if (intent.action == actionUnmute) {
|
||||||
if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||||
|
BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
||||||
|
}
|
||||||
} else if (intent.action == actionMute) {
|
} else if (intent.action == actionMute) {
|
||||||
if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true)
|
if (!BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||||
|
BooleanSetting.AUDIO_MUTED.setBoolean(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildPictureInPictureParams()
|
buildPictureInPictureParams()
|
||||||
}
|
}
|
||||||
@ -423,7 +433,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
}
|
}
|
||||||
// Always resume audio, since there is no UI button
|
// Always resume audio, since there is no UI button
|
||||||
if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
||||||
|
BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>(
|
||||||
|
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||||
|
) {
|
||||||
|
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) {
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
|
||||||
|
}
|
||||||
|
binding.title.text = addon.title
|
||||||
|
binding.version.text = addon.version
|
||||||
|
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
||||||
|
addon.enabled = checked
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
|
|||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding
|
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
|
||||||
import org.yuzu.yuzu_emu.model.Applet
|
import org.yuzu.yuzu_emu.model.Applet
|
||||||
import org.yuzu.yuzu_emu.model.AppletInfo
|
import org.yuzu.yuzu_emu.model.AppletInfo
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
|
|||||||
parent: ViewGroup,
|
parent: ViewGroup,
|
||||||
viewType: Int
|
viewType: Int
|
||||||
): AppletAdapter.AppletViewHolder {
|
): AppletAdapter.AppletViewHolder {
|
||||||
CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
.apply { root.setOnClickListener(this@AppletAdapter) }
|
.apply { root.setOnClickListener(this@AppletAdapter) }
|
||||||
.also { return AppletViewHolder(it) }
|
.also { return AppletViewHolder(it) }
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
|
|||||||
view.findNavController().navigate(action)
|
view.findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AppletViewHolder(val binding: CardAppletOptionBinding) :
|
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
lateinit var applet: Applet
|
lateinit var applet: Applet
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
|
|||||||
if (driverViewModel.selectedDriver > position) {
|
if (driverViewModel.selectedDriver > position) {
|
||||||
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
|
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
|
||||||
}
|
}
|
||||||
if (GpuDriverHelper.customDriverData == driverData.second) {
|
if (GpuDriverHelper.customDriverSettingData == driverData.second) {
|
||||||
driverViewModel.setSelectedDriverIndex(0)
|
driverViewModel.setSelectedDriverIndex(0)
|
||||||
}
|
}
|
||||||
driverViewModel.driversToDelete.add(driverData.first)
|
driverViewModel.driversToDelete.add(driverData.first)
|
||||||
|
@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
|
|||||||
|
|
||||||
class GameAdapter(private val activity: AppCompatActivity) :
|
class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||||
View.OnClickListener {
|
View.OnClickListener,
|
||||||
|
View.OnLongClickListener {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||||
// Create a new view.
|
// Create a new view.
|
||||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
binding.cardGame.setOnClickListener(this)
|
binding.cardGame.setOnClickListener(this)
|
||||||
|
binding.cardGame.setOnLongClickListener(this)
|
||||||
|
|
||||||
// Use that view to create a ViewHolder.
|
// Use that view to create a ViewHolder.
|
||||||
return GameViewHolder(binding)
|
return GameViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
|
||||||
holder.bind(currentList[position])
|
holder.bind(currentList[position])
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = currentList.size
|
override fun getItemCount(): Int = currentList.size
|
||||||
|
|
||||||
@ -125,10 +126,17 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
|
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
|
||||||
view.findNavController().navigate(action)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
lateinit var game: Game
|
lateinit var game: Game
|
||||||
@ -157,7 +165,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
|||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||||
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
return oldItem.programId == newItem.programId
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
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
|
||||||
|
|
||||||
|
class GamePropertiesAdapter(
|
||||||
|
private val viewLifecycle: LifecycleOwner,
|
||||||
|
private var properties: List<GameProperty>
|
||||||
|
) :
|
||||||
|
RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): GamePropertyViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
return when (viewType) {
|
||||||
|
PropertyType.Submenu.ordinal -> {
|
||||||
|
SubmenuPropertyViewHolder(
|
||||||
|
CardSimpleOutlinedBinding.inflate(
|
||||||
|
inflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> InstallablePropertyViewHolder(
|
||||||
|
CardInstallableIconBinding.inflate(
|
||||||
|
inflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
else -> PropertyType.Installable.ordinal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
submenuProperty.action.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.title.setText(submenuProperty.titleId)
|
||||||
|
binding.description.setText(submenuProperty.descriptionId)
|
||||||
|
binding.icon.setImageDrawable(
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
binding.icon.context.resources,
|
||||||
|
submenuProperty.iconId,
|
||||||
|
binding.icon.context.theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.details.postDelayed({
|
||||||
|
binding.details.isSelected = true
|
||||||
|
binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
if (submenuProperty.details != null) {
|
||||||
|
binding.details.visibility = View.VISIBLE
|
||||||
|
binding.details.text = submenuProperty.details.invoke()
|
||||||
|
} else if (submenuProperty.detailsFlow != null) {
|
||||||
|
binding.details.visibility = View.VISIBLE
|
||||||
|
viewLifecycle.lifecycleScope.launch {
|
||||||
|
viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
submenuProperty.detailsFlow.collect { binding.details.text = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.details.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
|
||||||
|
GamePropertyViewHolder(binding.root) {
|
||||||
|
override fun bind(property: GameProperty) {
|
||||||
|
val installableProperty = property as InstallableProperty
|
||||||
|
|
||||||
|
binding.title.setText(installableProperty.titleId)
|
||||||
|
binding.description.setText(installableProperty.descriptionId)
|
||||||
|
binding.icon.setImageDrawable(
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
binding.icon.context.resources,
|
||||||
|
installableProperty.iconId,
|
||||||
|
binding.icon.context.theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (installableProperty.install != null) {
|
||||||
|
binding.buttonInstall.visibility = View.VISIBLE
|
||||||
|
binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() }
|
||||||
|
}
|
||||||
|
if (installableProperty.export != null) {
|
||||||
|
binding.buttonExport.visibility = View.VISIBLE
|
||||||
|
binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PropertyType {
|
||||||
|
Submenu,
|
||||||
|
Installable
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@
|
|||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
interface AbstractBooleanSetting : AbstractSetting {
|
interface AbstractBooleanSetting : AbstractSetting {
|
||||||
val boolean: Boolean
|
fun getBoolean(needsGlobal: Boolean = false): Boolean
|
||||||
|
|
||||||
fun setBoolean(value: Boolean)
|
fun setBoolean(value: Boolean)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
interface AbstractByteSetting : AbstractSetting {
|
interface AbstractByteSetting : AbstractSetting {
|
||||||
val byte: Byte
|
fun getByte(needsGlobal: Boolean = false): Byte
|
||||||
|
|
||||||
fun setByte(value: Byte)
|
fun setByte(value: Byte)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
interface AbstractFloatSetting : AbstractSetting {
|
interface AbstractFloatSetting : AbstractSetting {
|
||||||
val float: Float
|
fun getFloat(needsGlobal: Boolean = false): Float
|
||||||
|
|
||||||
fun setFloat(value: Float)
|
fun setFloat(value: Float)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
interface AbstractIntSetting : AbstractSetting {
|
interface AbstractIntSetting : AbstractSetting {
|
||||||
val int: Int
|
fun getInt(needsGlobal: Boolean = false): Int
|
||||||
|
|
||||||
fun setInt(value: Int)
|
fun setInt(value: Int)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
interface AbstractLongSetting : AbstractSetting {
|
interface AbstractLongSetting : AbstractSetting {
|
||||||
val long: Long
|
fun getLong(needsGlobal: Boolean = false): Long
|
||||||
|
|
||||||
fun setLong(value: Long)
|
fun setLong(value: Long)
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
|
|||||||
|
|
||||||
interface AbstractSetting {
|
interface AbstractSetting {
|
||||||
val key: String
|
val key: String
|
||||||
val category: Settings.Category
|
|
||||||
val defaultValue: Any
|
val defaultValue: Any
|
||||||
val androidDefault: Any?
|
|
||||||
get() = null
|
|
||||||
val valueAsString: String
|
|
||||||
get() = ""
|
|
||||||
|
|
||||||
val isRuntimeModifiable: Boolean
|
val isRuntimeModifiable: Boolean
|
||||||
get() = NativeConfig.getIsRuntimeModifiable(key)
|
get() = NativeConfig.getIsRuntimeModifiable(key)
|
||||||
@ -20,5 +15,17 @@ interface AbstractSetting {
|
|||||||
val pairedSettingKey: String
|
val pairedSettingKey: String
|
||||||
get() = NativeConfig.getPairedSettingKey(key)
|
get() = NativeConfig.getPairedSettingKey(key)
|
||||||
|
|
||||||
|
val isSwitchable: Boolean
|
||||||
|
get() = NativeConfig.getIsSwitchable(key)
|
||||||
|
|
||||||
|
var global: Boolean
|
||||||
|
get() = NativeConfig.usingGlobal(key)
|
||||||
|
set(value) = NativeConfig.setGlobal(key, value)
|
||||||
|
|
||||||
|
val isSaveable: Boolean
|
||||||
|
get() = NativeConfig.getIsSaveable(key)
|
||||||
|
|
||||||
|
fun getValueAsString(needsGlobal: Boolean = false): String
|
||||||
|
|
||||||
fun reset()
|
fun reset()
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
interface AbstractShortSetting : AbstractSetting {
|
interface AbstractShortSetting : AbstractSetting {
|
||||||
val short: Short
|
fun getShort(needsGlobal: Boolean = false): Short
|
||||||
|
|
||||||
fun setShort(value: Short)
|
fun setShort(value: Short)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package org.yuzu.yuzu_emu.features.settings.model
|
package org.yuzu.yuzu_emu.features.settings.model
|
||||||
|
|
||||||
interface AbstractStringSetting : AbstractSetting {
|
interface AbstractStringSetting : AbstractSetting {
|
||||||
val string: String
|
fun getString(needsGlobal: Boolean = false): String
|
||||||
|
|
||||||
fun setString(value: String)
|
fun setString(value: String)
|
||||||
}
|
}
|
||||||
|
@ -5,36 +5,41 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
enum class BooleanSetting(
|
enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||||
override val key: String,
|
AUDIO_MUTED("audio_muted"),
|
||||||
override val category: Settings.Category,
|
CPU_DEBUG_MODE("cpu_debug_mode"),
|
||||||
override val androidDefault: Boolean? = null
|
FASTMEM("cpuopt_fastmem"),
|
||||||
) : AbstractBooleanSetting {
|
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
|
||||||
AUDIO_MUTED("audio_muted", Settings.Category.Audio),
|
RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
|
||||||
CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu),
|
USE_DOCKED_MODE("use_docked_mode"),
|
||||||
FASTMEM("cpuopt_fastmem", Settings.Category.Cpu),
|
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"),
|
||||||
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu),
|
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
|
||||||
RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core),
|
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
||||||
USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false),
|
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
||||||
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer),
|
RENDERER_DEBUG("debug"),
|
||||||
RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer),
|
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||||
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer),
|
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false),
|
BLACK_BACKGROUNDS("black_backgrounds"),
|
||||||
RENDERER_DEBUG("debug", Settings.Category.Renderer),
|
JOYSTICK_REL_CENTER("joystick_rel_center"),
|
||||||
PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android),
|
DPAD_SLIDE("dpad_slide"),
|
||||||
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System);
|
HAPTIC_FEEDBACK("haptic_feedback"),
|
||||||
|
SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"),
|
||||||
|
SHOW_INPUT_OVERLAY("show_input_overlay"),
|
||||||
|
TOUCHSCREEN("touchscreen");
|
||||||
|
|
||||||
override val boolean: Boolean
|
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
||||||
get() = NativeConfig.getBoolean(key, false)
|
NativeConfig.getBoolean(key, needsGlobal)
|
||||||
|
|
||||||
override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value)
|
override fun setBoolean(value: Boolean) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
override val defaultValue: Boolean by lazy {
|
global = false
|
||||||
androidDefault ?: NativeConfig.getBoolean(key, true)
|
}
|
||||||
|
NativeConfig.setBoolean(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val valueAsString: String
|
override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() }
|
||||||
get() = if (boolean) "1" else "0"
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString()
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setBoolean(key, defaultValue)
|
override fun reset() = NativeConfig.setBoolean(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
enum class ByteSetting(
|
enum class ByteSetting(override val key: String) : AbstractByteSetting {
|
||||||
override val key: String,
|
AUDIO_VOLUME("volume");
|
||||||
override val category: Settings.Category
|
|
||||||
) : AbstractByteSetting {
|
|
||||||
AUDIO_VOLUME("volume", Settings.Category.Audio);
|
|
||||||
|
|
||||||
override val byte: Byte
|
override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
|
||||||
get() = NativeConfig.getByte(key, false)
|
|
||||||
|
|
||||||
override fun setByte(value: Byte) = NativeConfig.setByte(key, value)
|
override fun setByte(value: Byte) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
global = false
|
||||||
|
}
|
||||||
|
NativeConfig.setByte(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) }
|
override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() }
|
||||||
|
|
||||||
override val valueAsString: String
|
override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
|
||||||
get() = byte.toString()
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setByte(key, defaultValue)
|
override fun reset() = NativeConfig.setByte(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
enum class FloatSetting(
|
enum class FloatSetting(override val key: String) : AbstractFloatSetting {
|
||||||
override val key: String,
|
|
||||||
override val category: Settings.Category
|
|
||||||
) : AbstractFloatSetting {
|
|
||||||
// No float settings currently exist
|
// No float settings currently exist
|
||||||
EMPTY_SETTING("", Settings.Category.UiGeneral);
|
EMPTY_SETTING("");
|
||||||
|
|
||||||
override val float: Float
|
override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
|
||||||
get() = NativeConfig.getFloat(key, false)
|
|
||||||
|
|
||||||
override fun setFloat(value: Float) = NativeConfig.setFloat(key, value)
|
override fun setFloat(value: Float) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
global = false
|
||||||
|
}
|
||||||
|
NativeConfig.setFloat(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) }
|
override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() }
|
||||||
|
|
||||||
override val valueAsString: String
|
override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
|
||||||
get() = float.toString()
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setFloat(key, defaultValue)
|
override fun reset() = NativeConfig.setFloat(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
@ -5,36 +5,38 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
enum class IntSetting(
|
enum class IntSetting(override val key: String) : AbstractIntSetting {
|
||||||
override val key: String,
|
CPU_BACKEND("cpu_backend"),
|
||||||
override val category: Settings.Category,
|
CPU_ACCURACY("cpu_accuracy"),
|
||||||
override val androidDefault: Int? = null
|
REGION_INDEX("region_index"),
|
||||||
) : AbstractIntSetting {
|
LANGUAGE_INDEX("language_index"),
|
||||||
CPU_BACKEND("cpu_backend", Settings.Category.Cpu),
|
RENDERER_BACKEND("backend"),
|
||||||
CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
|
RENDERER_ACCURACY("gpu_accuracy"),
|
||||||
REGION_INDEX("region_index", Settings.Category.System),
|
RENDERER_RESOLUTION("resolution_setup"),
|
||||||
LANGUAGE_INDEX("language_index", Settings.Category.System),
|
RENDERER_VSYNC("use_vsync"),
|
||||||
RENDERER_BACKEND("backend", Settings.Category.Renderer),
|
RENDERER_SCALING_FILTER("scaling_filter"),
|
||||||
RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0),
|
RENDERER_ANTI_ALIASING("anti_aliasing"),
|
||||||
RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer),
|
RENDERER_SCREEN_LAYOUT("screen_layout"),
|
||||||
RENDERER_VSYNC("use_vsync", Settings.Category.Renderer),
|
RENDERER_ASPECT_RATIO("aspect_ratio"),
|
||||||
RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer),
|
AUDIO_OUTPUT_ENGINE("output_engine"),
|
||||||
RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer),
|
MAX_ANISOTROPY("max_anisotropy"),
|
||||||
RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android),
|
THEME("theme"),
|
||||||
RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer),
|
THEME_MODE("theme_mode"),
|
||||||
AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio);
|
OVERLAY_SCALE("control_scale"),
|
||||||
|
OVERLAY_OPACITY("control_opacity");
|
||||||
|
|
||||||
override val int: Int
|
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
|
||||||
get() = NativeConfig.getInt(key, false)
|
|
||||||
|
|
||||||
override fun setInt(value: Int) = NativeConfig.setInt(key, value)
|
override fun setInt(value: Int) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
override val defaultValue: Int by lazy {
|
global = false
|
||||||
androidDefault ?: NativeConfig.getInt(key, true)
|
}
|
||||||
|
NativeConfig.setInt(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val valueAsString: String
|
override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
|
||||||
get() = int.toString()
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setInt(key, defaultValue)
|
override fun reset() = NativeConfig.setInt(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
enum class LongSetting(
|
enum class LongSetting(override val key: String) : AbstractLongSetting {
|
||||||
override val key: String,
|
CUSTOM_RTC("custom_rtc");
|
||||||
override val category: Settings.Category
|
|
||||||
) : AbstractLongSetting {
|
|
||||||
CUSTOM_RTC("custom_rtc", Settings.Category.System);
|
|
||||||
|
|
||||||
override val long: Long
|
override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
|
||||||
get() = NativeConfig.getLong(key, false)
|
|
||||||
|
|
||||||
override fun setLong(value: Long) = NativeConfig.setLong(key, value)
|
override fun setLong(value: Long) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
global = false
|
||||||
|
}
|
||||||
|
NativeConfig.setLong(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) }
|
override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() }
|
||||||
|
|
||||||
override val valueAsString: String
|
override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
|
||||||
get() = long.toString()
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setLong(key, defaultValue)
|
override fun reset() = NativeConfig.setLong(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
@ -6,78 +6,19 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
|
|
||||||
object Settings {
|
object Settings {
|
||||||
enum class Category {
|
|
||||||
Android,
|
|
||||||
Audio,
|
|
||||||
Core,
|
|
||||||
Cpu,
|
|
||||||
CpuDebug,
|
|
||||||
CpuUnsafe,
|
|
||||||
Renderer,
|
|
||||||
RendererAdvanced,
|
|
||||||
RendererDebug,
|
|
||||||
System,
|
|
||||||
SystemAudio,
|
|
||||||
DataStorage,
|
|
||||||
Debugging,
|
|
||||||
DebuggingGraphics,
|
|
||||||
Miscellaneous,
|
|
||||||
Network,
|
|
||||||
WebService,
|
|
||||||
AddOns,
|
|
||||||
Controls,
|
|
||||||
Ui,
|
|
||||||
UiGeneral,
|
|
||||||
UiLayout,
|
|
||||||
UiGameList,
|
|
||||||
Screenshots,
|
|
||||||
Shortcuts,
|
|
||||||
Multiplayer,
|
|
||||||
Services,
|
|
||||||
Paths,
|
|
||||||
MaxEnum
|
|
||||||
}
|
|
||||||
|
|
||||||
val settingsList = listOf<AbstractSetting>(
|
|
||||||
*BooleanSetting.values(),
|
|
||||||
*ByteSetting.values(),
|
|
||||||
*ShortSetting.values(),
|
|
||||||
*IntSetting.values(),
|
|
||||||
*FloatSetting.values(),
|
|
||||||
*LongSetting.values(),
|
|
||||||
*StringSetting.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
const val SECTION_GENERAL = "General"
|
|
||||||
const val SECTION_SYSTEM = "System"
|
|
||||||
const val SECTION_RENDERER = "Renderer"
|
|
||||||
const val SECTION_AUDIO = "Audio"
|
|
||||||
const val SECTION_CPU = "Cpu"
|
|
||||||
const val SECTION_THEME = "Theme"
|
|
||||||
const val SECTION_DEBUG = "Debug"
|
|
||||||
|
|
||||||
enum class MenuTag(val titleId: Int) {
|
enum class MenuTag(val titleId: Int) {
|
||||||
SECTION_ROOT(R.string.advanced_settings),
|
SECTION_ROOT(R.string.advanced_settings),
|
||||||
SECTION_SYSTEM(R.string.preferences_system),
|
SECTION_SYSTEM(R.string.preferences_system),
|
||||||
SECTION_RENDERER(R.string.preferences_graphics),
|
SECTION_RENDERER(R.string.preferences_graphics),
|
||||||
SECTION_AUDIO(R.string.preferences_audio),
|
SECTION_AUDIO(R.string.preferences_audio),
|
||||||
SECTION_CPU(R.string.cpu),
|
|
||||||
SECTION_THEME(R.string.preferences_theme),
|
SECTION_THEME(R.string.preferences_theme),
|
||||||
SECTION_DEBUG(R.string.preferences_debug);
|
SECTION_DEBUG(R.string.preferences_debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
||||||
const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
|
const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
|
||||||
|
|
||||||
const val PREF_OVERLAY_VERSION = "OverlayVersion"
|
// Deprecated input overlay preference keys
|
||||||
const val PREF_LANDSCAPE_OVERLAY_VERSION = "LandscapeOverlayVersion"
|
|
||||||
const val PREF_PORTRAIT_OVERLAY_VERSION = "PortraitOverlayVersion"
|
|
||||||
const val PREF_FOLDABLE_OVERLAY_VERSION = "FoldableOverlayVersion"
|
|
||||||
val overlayLayoutPrefs = listOf(
|
|
||||||
PREF_LANDSCAPE_OVERLAY_VERSION,
|
|
||||||
PREF_PORTRAIT_OVERLAY_VERSION,
|
|
||||||
PREF_FOLDABLE_OVERLAY_VERSION
|
|
||||||
)
|
|
||||||
|
|
||||||
const val PREF_CONTROL_SCALE = "controlScale"
|
const val PREF_CONTROL_SCALE = "controlScale"
|
||||||
const val PREF_CONTROL_OPACITY = "controlOpacity"
|
const val PREF_CONTROL_OPACITY = "controlOpacity"
|
||||||
const val PREF_TOUCH_ENABLED = "isTouchEnabled"
|
const val PREF_TOUCH_ENABLED = "isTouchEnabled"
|
||||||
@ -98,23 +39,12 @@ object Settings {
|
|||||||
const val PREF_BUTTON_STICK_R = "buttonToggle14"
|
const val PREF_BUTTON_STICK_R = "buttonToggle14"
|
||||||
const val PREF_BUTTON_HOME = "buttonToggle15"
|
const val PREF_BUTTON_HOME = "buttonToggle15"
|
||||||
const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
|
const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
|
||||||
|
|
||||||
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
|
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
|
||||||
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
|
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
|
||||||
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
|
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
|
||||||
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
|
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
|
||||||
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
|
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
|
||||||
|
|
||||||
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
|
||||||
const val PREF_THEME = "Theme"
|
|
||||||
const val PREF_THEME_MODE = "ThemeMode"
|
|
||||||
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
|
|
||||||
|
|
||||||
val overlayPreferences = listOf(
|
val overlayPreferences = listOf(
|
||||||
PREF_OVERLAY_VERSION,
|
|
||||||
PREF_CONTROL_SCALE,
|
|
||||||
PREF_CONTROL_OPACITY,
|
|
||||||
PREF_TOUCH_ENABLED,
|
|
||||||
PREF_BUTTON_A,
|
PREF_BUTTON_A,
|
||||||
PREF_BUTTON_B,
|
PREF_BUTTON_B,
|
||||||
PREF_BUTTON_X,
|
PREF_BUTTON_X,
|
||||||
@ -134,6 +64,21 @@ object Settings {
|
|||||||
PREF_BUTTON_STICK_R
|
PREF_BUTTON_STICK_R
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Deprecated layout preference keys
|
||||||
|
const val PREF_LANDSCAPE_SUFFIX = "_Landscape"
|
||||||
|
const val PREF_PORTRAIT_SUFFIX = "_Portrait"
|
||||||
|
const val PREF_FOLDABLE_SUFFIX = "_Foldable"
|
||||||
|
val overlayLayoutSuffixes = listOf(
|
||||||
|
PREF_LANDSCAPE_SUFFIX,
|
||||||
|
PREF_PORTRAIT_SUFFIX,
|
||||||
|
PREF_FOLDABLE_SUFFIX
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deprecated theme preference keys
|
||||||
|
const val PREF_THEME = "Theme"
|
||||||
|
const val PREF_THEME_MODE = "ThemeMode"
|
||||||
|
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
|
||||||
|
|
||||||
const val LayoutOption_Unspecified = 0
|
const val LayoutOption_Unspecified = 0
|
||||||
const val LayoutOption_MobilePortrait = 4
|
const val LayoutOption_MobilePortrait = 4
|
||||||
const val LayoutOption_MobileLandscape = 5
|
const val LayoutOption_MobileLandscape = 5
|
||||||
|
@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
enum class ShortSetting(
|
enum class ShortSetting(override val key: String) : AbstractShortSetting {
|
||||||
override val key: String,
|
RENDERER_SPEED_LIMIT("speed_limit");
|
||||||
override val category: Settings.Category
|
|
||||||
) : AbstractShortSetting {
|
|
||||||
RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
|
|
||||||
|
|
||||||
override val short: Short
|
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
|
||||||
get() = NativeConfig.getShort(key, false)
|
|
||||||
|
|
||||||
override fun setShort(value: Short) = NativeConfig.setShort(key, value)
|
override fun setShort(value: Short) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
global = false
|
||||||
|
}
|
||||||
|
NativeConfig.setShort(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) }
|
override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() }
|
||||||
|
|
||||||
override val valueAsString: String
|
override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
|
||||||
get() = short.toString()
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setShort(key, defaultValue)
|
override fun reset() = NativeConfig.setShort(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
|
|||||||
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
enum class StringSetting(
|
enum class StringSetting(override val key: String) : AbstractStringSetting {
|
||||||
override val key: String,
|
DRIVER_PATH("driver_path");
|
||||||
override val category: Settings.Category
|
|
||||||
) : AbstractStringSetting {
|
|
||||||
// No string settings currently exist
|
|
||||||
EMPTY_SETTING("", Settings.Category.UiGeneral);
|
|
||||||
|
|
||||||
override val string: String
|
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
|
||||||
get() = NativeConfig.getString(key, false)
|
|
||||||
|
|
||||||
override fun setString(value: String) = NativeConfig.setString(key, value)
|
override fun setString(value: String) {
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
global = false
|
||||||
|
}
|
||||||
|
NativeConfig.setString(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultValue: String by lazy { NativeConfig.getString(key, true) }
|
override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) }
|
||||||
|
|
||||||
override val valueAsString: String
|
override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
|
||||||
get() = string
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setString(key, defaultValue)
|
override fun reset() = NativeConfig.setString(key, defaultValue)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ class DateTimeSetting(
|
|||||||
) : SettingsItem(longSetting, titleId, descriptionId) {
|
) : SettingsItem(longSetting, titleId, descriptionId) {
|
||||||
override val type = TYPE_DATETIME_SETTING
|
override val type = TYPE_DATETIME_SETTING
|
||||||
|
|
||||||
var value: Long
|
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
|
||||||
get() = longSetting.long
|
fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
|
||||||
set(value) = (setting as AbstractLongSetting).setLong(value)
|
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
|
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
|
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
|
||||||
@ -30,10 +30,26 @@ abstract class SettingsItem(
|
|||||||
|
|
||||||
val isEditable: Boolean
|
val isEditable: Boolean
|
||||||
get() {
|
get() {
|
||||||
|
// Can't edit settings that aren't saveable in per-game config even if they are switchable
|
||||||
|
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (!NativeLibrary.isRunning()) return true
|
if (!NativeLibrary.isRunning()) return true
|
||||||
|
|
||||||
|
// Prevent editing settings that were modified in per-game config while editing global
|
||||||
|
// config
|
||||||
|
if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return setting.isRuntimeModifiable
|
return setting.isRuntimeModifiable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val needsRuntimeGlobal: Boolean
|
||||||
|
get() = NativeLibrary.isRunning() && !setting.global &&
|
||||||
|
!NativeConfig.isPerGameConfigLoaded()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TYPE_HEADER = 0
|
const val TYPE_HEADER = 0
|
||||||
const val TYPE_SWITCH = 1
|
const val TYPE_SWITCH = 1
|
||||||
@ -48,8 +64,9 @@ abstract class SettingsItem(
|
|||||||
|
|
||||||
val emptySetting = object : AbstractSetting {
|
val emptySetting = object : AbstractSetting {
|
||||||
override val key: String = ""
|
override val key: String = ""
|
||||||
override val category: Settings.Category = Settings.Category.Ui
|
|
||||||
override val defaultValue: Any = false
|
override val defaultValue: Any = false
|
||||||
|
override val isSaveable = true
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String = ""
|
||||||
override fun reset() {}
|
override fun reset() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +243,15 @@ abstract class SettingsItem(
|
|||||||
R.string.renderer_reactive_flushing_description
|
R.string.renderer_reactive_flushing_description
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
put(
|
||||||
|
SingleChoiceSetting(
|
||||||
|
IntSetting.MAX_ANISOTROPY,
|
||||||
|
R.string.anisotropic_filtering,
|
||||||
|
R.string.anisotropic_filtering_description,
|
||||||
|
R.array.anisoEntries,
|
||||||
|
R.array.anisoValues
|
||||||
|
)
|
||||||
|
)
|
||||||
put(
|
put(
|
||||||
SingleChoiceSetting(
|
SingleChoiceSetting(
|
||||||
IntSetting.AUDIO_OUTPUT_ENGINE,
|
IntSetting.AUDIO_OUTPUT_ENGINE,
|
||||||
@ -270,9 +296,9 @@ abstract class SettingsItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val fastmem = object : AbstractBooleanSetting {
|
val fastmem = object : AbstractBooleanSetting {
|
||||||
override val boolean: Boolean
|
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
||||||
get() =
|
BooleanSetting.FASTMEM.getBoolean() &&
|
||||||
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
|
BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean()
|
||||||
|
|
||||||
override fun setBoolean(value: Boolean) {
|
override fun setBoolean(value: Boolean) {
|
||||||
BooleanSetting.FASTMEM.setBoolean(value)
|
BooleanSetting.FASTMEM.setBoolean(value)
|
||||||
@ -280,9 +306,25 @@ abstract class SettingsItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val key: String = FASTMEM_COMBINED
|
override val key: String = FASTMEM_COMBINED
|
||||||
override val category = Settings.Category.Cpu
|
|
||||||
override val isRuntimeModifiable: Boolean = false
|
override val isRuntimeModifiable: Boolean = false
|
||||||
|
override val pairedSettingKey = BooleanSetting.CPU_DEBUG_MODE.key
|
||||||
override val defaultValue: Boolean = true
|
override val defaultValue: Boolean = true
|
||||||
|
override val isSwitchable: Boolean = true
|
||||||
|
override var global: Boolean
|
||||||
|
get() {
|
||||||
|
return BooleanSetting.FASTMEM.global &&
|
||||||
|
BooleanSetting.FASTMEM_EXCLUSIVES.global
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
BooleanSetting.FASTMEM.global = value
|
||||||
|
BooleanSetting.FASTMEM_EXCLUSIVES.global = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isSaveable = true
|
||||||
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String =
|
||||||
|
getBoolean().toString()
|
||||||
|
|
||||||
override fun reset() = setBoolean(defaultValue)
|
override fun reset() = setBoolean(defaultValue)
|
||||||
}
|
}
|
||||||
put(SwitchSetting(fastmem, R.string.fastmem, 0))
|
put(SwitchSetting(fastmem, R.string.fastmem, 0))
|
||||||
|
@ -15,16 +15,11 @@ class SingleChoiceSetting(
|
|||||||
) : SettingsItem(setting, titleId, descriptionId) {
|
) : SettingsItem(setting, titleId, descriptionId) {
|
||||||
override val type = TYPE_SINGLE_CHOICE
|
override val type = TYPE_SINGLE_CHOICE
|
||||||
|
|
||||||
var selectedValue: Int
|
fun getSelectedValue(needsGlobal: Boolean = false) =
|
||||||
get() {
|
when (setting) {
|
||||||
return when (setting) {
|
is AbstractIntSetting -> setting.getInt(needsGlobal)
|
||||||
is AbstractIntSetting -> setting.int
|
|
||||||
else -> -1
|
else -> -1
|
||||||
}
|
}
|
||||||
}
|
|
||||||
set(value) {
|
fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
|
||||||
when (setting) {
|
|
||||||
is AbstractIntSetting -> setting.setInt(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -20,22 +20,20 @@ class SliderSetting(
|
|||||||
) : SettingsItem(setting, titleId, descriptionId) {
|
) : SettingsItem(setting, titleId, descriptionId) {
|
||||||
override val type = TYPE_SLIDER
|
override val type = TYPE_SLIDER
|
||||||
|
|
||||||
var selectedValue: Int
|
fun getSelectedValue(needsGlobal: Boolean = false) =
|
||||||
get() {
|
when (setting) {
|
||||||
return when (setting) {
|
is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
|
||||||
is AbstractByteSetting -> setting.byte.toInt()
|
is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
|
||||||
is AbstractShortSetting -> setting.short.toInt()
|
is AbstractIntSetting -> setting.getInt(needsGlobal)
|
||||||
is AbstractIntSetting -> setting.int
|
is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt()
|
||||||
is AbstractFloatSetting -> setting.float.roundToInt()
|
|
||||||
else -> -1
|
else -> -1
|
||||||
}
|
}
|
||||||
}
|
|
||||||
set(value) {
|
fun setSelectedValue(value: Int) =
|
||||||
when (setting) {
|
when (setting) {
|
||||||
is AbstractByteSetting -> setting.setByte(value.toByte())
|
is AbstractByteSetting -> setting.setByte(value.toByte())
|
||||||
is AbstractShortSetting -> setting.setShort(value.toShort())
|
is AbstractShortSetting -> setting.setShort(value.toShort())
|
||||||
is AbstractIntSetting -> setting.setInt(value)
|
|
||||||
is AbstractFloatSetting -> setting.setFloat(value.toFloat())
|
is AbstractFloatSetting -> setting.setFloat(value.toFloat())
|
||||||
}
|
else -> (setting as AbstractIntSetting).setInt(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,14 +17,13 @@ class StringSingleChoiceSetting(
|
|||||||
fun getValueAt(index: Int): String =
|
fun getValueAt(index: Int): String =
|
||||||
if (index >= 0 && index < values.size) values[index] else ""
|
if (index >= 0 && index < values.size) values[index] else ""
|
||||||
|
|
||||||
var selectedValue: String
|
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
|
||||||
get() = stringSetting.string
|
fun setSelectedValue(value: String) = stringSetting.setString(value)
|
||||||
set(value) = stringSetting.setString(value)
|
|
||||||
|
|
||||||
val selectValueIndex: Int
|
val selectValueIndex: Int
|
||||||
get() {
|
get() {
|
||||||
for (i in values.indices) {
|
for (i in values.indices) {
|
||||||
if (values[i] == selectedValue) {
|
if (values[i] == getSelectedValue()) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,15 +14,15 @@ class SwitchSetting(
|
|||||||
) : SettingsItem(setting, titleId, descriptionId) {
|
) : SettingsItem(setting, titleId, descriptionId) {
|
||||||
override val type = TYPE_SWITCH
|
override val type = TYPE_SWITCH
|
||||||
|
|
||||||
var checked: Boolean
|
fun getIsChecked(needsGlobal: Boolean = false): Boolean {
|
||||||
get() {
|
|
||||||
return when (setting) {
|
return when (setting) {
|
||||||
is AbstractIntSetting -> setting.int == 1
|
is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
|
||||||
is AbstractBooleanSetting -> setting.boolean
|
is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set(value) {
|
|
||||||
|
fun setChecked(value: Boolean) {
|
||||||
when (setting) {
|
when (setting) {
|
||||||
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
|
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
|
||||||
is AbstractBooleanSetting -> setting.setBoolean(value)
|
is AbstractBooleanSetting -> setting.setBoolean(value)
|
||||||
|
@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
||||||
@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
|
||||||
|
SettingsFile.loadCustomConfig(args.game!!)
|
||||||
|
}
|
||||||
settingsViewModel.game = args.game
|
settingsViewModel.game = args.game
|
||||||
|
|
||||||
val navHostFragment =
|
val navHostFragment =
|
||||||
@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
// TODO: Load custom settings contextually
|
|
||||||
if (!DirectoryInitialization.areDirectoriesReady) {
|
if (!DirectoryInitialization.areDirectoriesReady) {
|
||||||
DirectoryInitialization.start()
|
DirectoryInitialization.start()
|
||||||
}
|
}
|
||||||
@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
||||||
NativeConfig.saveSettings()
|
if (isFinishing) {
|
||||||
|
NativeLibrary.applySettings()
|
||||||
|
if (args.game == null) {
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
} else if (NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
NativeLibrary.logSettings()
|
||||||
|
NativeConfig.savePerGameConfig()
|
||||||
|
NativeConfig.unloadPerGameConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
settingsViewModel.clear()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSettingsReset() {
|
fun onSettingsReset() {
|
||||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||||
NativeConfig.unloadConfig()
|
if (args.game == null) {
|
||||||
|
NativeConfig.unloadGlobalConfig()
|
||||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||||
if (!settingsFile.delete()) {
|
if (!settingsFile.delete()) {
|
||||||
throw IOException("Failed to delete $settingsFile")
|
throw IOException("Failed to delete $settingsFile")
|
||||||
}
|
}
|
||||||
NativeConfig.initializeConfig()
|
NativeConfig.initializeGlobalConfig()
|
||||||
|
} else {
|
||||||
|
NativeConfig.unloadPerGameConfig()
|
||||||
|
val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
|
||||||
|
if (!settingsFile.delete()) {
|
||||||
|
throw IOException("Failed to delete $settingsFile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
|
@ -102,8 +102,9 @@ class SettingsAdapter(
|
|||||||
return currentList[position].type
|
return currentList[position].type
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) {
|
||||||
item.checked = checked
|
item.setChecked(checked)
|
||||||
|
notifyItemChanged(position)
|
||||||
settingsViewModel.setShouldReloadSettingsList(true)
|
settingsViewModel.setShouldReloadSettingsList(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +127,7 @@ class SettingsAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onDateTimeClick(item: DateTimeSetting, position: Int) {
|
fun onDateTimeClick(item: DateTimeSetting, position: Int) {
|
||||||
val storedTime = item.value * 1000
|
val storedTime = item.getValue() * 1000
|
||||||
|
|
||||||
// Helper to extract hour and minute from epoch time
|
// Helper to extract hour and minute from epoch time
|
||||||
val calendar: Calendar = Calendar.getInstance()
|
val calendar: Calendar = Calendar.getInstance()
|
||||||
@ -159,9 +160,9 @@ class SettingsAdapter(
|
|||||||
var epochTime: Long = datePicker.selection!! / 1000
|
var epochTime: Long = datePicker.selection!! / 1000
|
||||||
epochTime += timePicker.hour.toLong() * 60 * 60
|
epochTime += timePicker.hour.toLong() * 60 * 60
|
||||||
epochTime += timePicker.minute.toLong() * 60
|
epochTime += timePicker.minute.toLong() * 60
|
||||||
if (item.value != epochTime) {
|
if (item.getValue() != epochTime) {
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
item.value = epochTime
|
item.setValue(epochTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
datePicker.show(
|
datePicker.show(
|
||||||
@ -195,6 +196,12 @@ class SettingsAdapter(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onClearClick(item: SettingsItem, position: Int) {
|
||||||
|
item.setting.global = true
|
||||||
|
notifyItemChanged(position)
|
||||||
|
settingsViewModel.setShouldReloadSettingsList(true)
|
||||||
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
|
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
|
||||||
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
|
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
|
||||||
return oldItem.setting.key == newItem.setting.key
|
return oldItem.setting.key == newItem.setting.key
|
||||||
|
@ -66,7 +66,13 @@ class SettingsFragment : Fragment() {
|
|||||||
args.menuTag
|
args.menuTag
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId)
|
binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT &&
|
||||||
|
args.game != null
|
||||||
|
) {
|
||||||
|
args.game!!.title
|
||||||
|
} else {
|
||||||
|
getString(args.menuTag.titleId)
|
||||||
|
}
|
||||||
binding.listSettings.apply {
|
binding.listSettings.apply {
|
||||||
adapter = settingsAdapter
|
adapter = settingsAdapter
|
||||||
layoutManager = LinearLayoutManager(requireContext())
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
@ -3,10 +3,9 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.features.settings.ui
|
package org.yuzu.yuzu_emu.features.settings.ui
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.PreferenceManager
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
|
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
|
||||||
@ -28,15 +27,27 @@ class SettingsFragmentPresenter(
|
|||||||
) {
|
) {
|
||||||
private var settingsList = ArrayList<SettingsItem>()
|
private var settingsList = ArrayList<SettingsItem>()
|
||||||
|
|
||||||
private val preferences: SharedPreferences
|
// Extension for altering settings list based on each setting's properties
|
||||||
get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
|
|
||||||
// Extension for populating settings list based on paired settings
|
|
||||||
fun ArrayList<SettingsItem>.add(key: String) {
|
fun ArrayList<SettingsItem>.add(key: String) {
|
||||||
val item = SettingsItem.settingsItems[key]!!
|
val item = SettingsItem.settingsItems[key]!!
|
||||||
|
if (settingsViewModel.game != null && !item.setting.isSwitchable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
|
||||||
|
item.setting.global = true
|
||||||
|
}
|
||||||
|
|
||||||
val pairedSettingKey = item.setting.pairedSettingKey
|
val pairedSettingKey = item.setting.pairedSettingKey
|
||||||
if (pairedSettingKey.isNotEmpty()) {
|
if (pairedSettingKey.isNotEmpty()) {
|
||||||
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
|
val pairedSettingValue = NativeConfig.getBoolean(
|
||||||
|
pairedSettingKey,
|
||||||
|
if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
!NativeConfig.usingGlobal(pairedSettingKey)
|
||||||
|
} else {
|
||||||
|
NativeConfig.usingGlobal(pairedSettingKey)
|
||||||
|
}
|
||||||
|
)
|
||||||
if (!pairedSettingValue) return
|
if (!pairedSettingValue) return
|
||||||
}
|
}
|
||||||
add(item)
|
add(item)
|
||||||
@ -133,6 +144,7 @@ class SettingsFragmentPresenter(
|
|||||||
add(IntSetting.RENDERER_VSYNC.key)
|
add(IntSetting.RENDERER_VSYNC.key)
|
||||||
add(IntSetting.RENDERER_SCALING_FILTER.key)
|
add(IntSetting.RENDERER_SCALING_FILTER.key)
|
||||||
add(IntSetting.RENDERER_ANTI_ALIASING.key)
|
add(IntSetting.RENDERER_ANTI_ALIASING.key)
|
||||||
|
add(IntSetting.MAX_ANISOTROPY.key)
|
||||||
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
|
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
|
||||||
add(IntSetting.RENDERER_ASPECT_RATIO.key)
|
add(IntSetting.RENDERER_ASPECT_RATIO.key)
|
||||||
add(BooleanSetting.PICTURE_IN_PICTURE.key)
|
add(BooleanSetting.PICTURE_IN_PICTURE.key)
|
||||||
@ -153,25 +165,19 @@ class SettingsFragmentPresenter(
|
|||||||
private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
|
private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
|
||||||
sl.apply {
|
sl.apply {
|
||||||
val theme: AbstractIntSetting = object : AbstractIntSetting {
|
val theme: AbstractIntSetting = object : AbstractIntSetting {
|
||||||
override val int: Int
|
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME.getInt()
|
||||||
get() = preferences.getInt(Settings.PREF_THEME, 0)
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
override fun setInt(value: Int) {
|
||||||
preferences.edit()
|
IntSetting.THEME.setInt(value)
|
||||||
.putInt(Settings.PREF_THEME, value)
|
|
||||||
.apply()
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
settingsViewModel.setShouldRecreate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val key: String = Settings.PREF_THEME
|
override val key: String = IntSetting.THEME.key
|
||||||
override val category = Settings.Category.UiGeneral
|
override val isRuntimeModifiable: Boolean = IntSetting.THEME.isRuntimeModifiable
|
||||||
override val isRuntimeModifiable: Boolean = false
|
override fun getValueAsString(needsGlobal: Boolean): String =
|
||||||
override val defaultValue: Int = 0
|
IntSetting.THEME.getValueAsString()
|
||||||
override fun reset() {
|
|
||||||
preferences.edit()
|
override val defaultValue: Int = IntSetting.THEME.defaultValue
|
||||||
.putInt(Settings.PREF_THEME, defaultValue)
|
override fun reset() = IntSetting.THEME.setInt(defaultValue)
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
@ -197,24 +203,22 @@ class SettingsFragmentPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val themeMode: AbstractIntSetting = object : AbstractIntSetting {
|
val themeMode: AbstractIntSetting = object : AbstractIntSetting {
|
||||||
override val int: Int
|
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME_MODE.getInt()
|
||||||
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
override fun setInt(value: Int) {
|
||||||
preferences.edit()
|
IntSetting.THEME_MODE.setInt(value)
|
||||||
.putInt(Settings.PREF_THEME_MODE, value)
|
|
||||||
.apply()
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
settingsViewModel.setShouldRecreate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val key: String = Settings.PREF_THEME_MODE
|
override val key: String = IntSetting.THEME_MODE.key
|
||||||
override val category = Settings.Category.UiGeneral
|
override val isRuntimeModifiable: Boolean =
|
||||||
override val isRuntimeModifiable: Boolean = false
|
IntSetting.THEME_MODE.isRuntimeModifiable
|
||||||
override val defaultValue: Int = -1
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String =
|
||||||
|
IntSetting.THEME_MODE.getValueAsString()
|
||||||
|
|
||||||
|
override val defaultValue: Int = IntSetting.THEME_MODE.defaultValue
|
||||||
override fun reset() {
|
override fun reset() {
|
||||||
preferences.edit()
|
IntSetting.THEME_MODE.setInt(defaultValue)
|
||||||
.putInt(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
|
|
||||||
.apply()
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
settingsViewModel.setShouldRecreate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,24 +234,25 @@ class SettingsFragmentPresenter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
|
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
|
||||||
override val boolean: Boolean
|
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
||||||
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
|
BooleanSetting.BLACK_BACKGROUNDS.getBoolean()
|
||||||
|
|
||||||
override fun setBoolean(value: Boolean) {
|
override fun setBoolean(value: Boolean) {
|
||||||
preferences.edit()
|
BooleanSetting.BLACK_BACKGROUNDS.setBoolean(value)
|
||||||
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
|
|
||||||
.apply()
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
settingsViewModel.setShouldRecreate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val key: String = Settings.PREF_BLACK_BACKGROUNDS
|
override val key: String = BooleanSetting.BLACK_BACKGROUNDS.key
|
||||||
override val category = Settings.Category.UiGeneral
|
override val isRuntimeModifiable: Boolean =
|
||||||
override val isRuntimeModifiable: Boolean = false
|
BooleanSetting.BLACK_BACKGROUNDS.isRuntimeModifiable
|
||||||
override val defaultValue: Boolean = false
|
|
||||||
|
override fun getValueAsString(needsGlobal: Boolean): String =
|
||||||
|
BooleanSetting.BLACK_BACKGROUNDS.getValueAsString()
|
||||||
|
|
||||||
|
override val defaultValue: Boolean = BooleanSetting.BLACK_BACKGROUNDS.defaultValue
|
||||||
override fun reset() {
|
override fun reset() {
|
||||||
preferences.edit()
|
BooleanSetting.BLACK_BACKGROUNDS
|
||||||
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
|
.setBoolean(BooleanSetting.BLACK_BACKGROUNDS.defaultValue)
|
||||||
.apply()
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
settingsViewModel.setShouldRecreate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
|
import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||||
SettingViewHolder(binding.root, adapter) {
|
SettingViewHolder(binding.root, adapter) {
|
||||||
@ -29,12 +30,23 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.textSettingValue.visibility = View.VISIBLE
|
binding.textSettingValue.visibility = View.VISIBLE
|
||||||
val epochTime = setting.value
|
val epochTime = setting.getValue()
|
||||||
val instant = Instant.ofEpochMilli(epochTime * 1000)
|
val instant = Instant.ofEpochMilli(epochTime * 1000)
|
||||||
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
|
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
|
||||||
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
||||||
binding.textSettingValue.text = dateFormatter.format(zonedTime)
|
binding.textSettingValue.text = dateFormatter.format(zonedTime)
|
||||||
|
|
||||||
|
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||||
|
!NativeConfig.isPerGameConfigLoaded()
|
||||||
|
) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
binding.buttonClear.setOnClickListener {
|
||||||
|
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
setStyle(setting.isEditable, binding)
|
setStyle(setting.isEditable, binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
|||||||
binding.textSettingDescription.visibility = View.GONE
|
binding.textSettingDescription.visibility = View.GONE
|
||||||
}
|
}
|
||||||
binding.textSettingValue.visibility = View.GONE
|
binding.textSettingValue.visibility = View.GONE
|
||||||
|
binding.buttonClear.visibility = View.GONE
|
||||||
|
|
||||||
setStyle(setting.isEditable, binding)
|
setStyle(setting.isEditable, binding)
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
|||||||
binding.textSettingName.alpha = opacity
|
binding.textSettingName.alpha = opacity
|
||||||
binding.textSettingDescription.alpha = opacity
|
binding.textSettingDescription.alpha = opacity
|
||||||
binding.textSettingValue.alpha = opacity
|
binding.textSettingValue.alpha = opacity
|
||||||
|
binding.buttonClear.isEnabled = isEditable
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
|
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
|
||||||
@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
|||||||
val opacity = if (isEditable) 1.0f else 0.5f
|
val opacity = if (isEditable) 1.0f else 0.5f
|
||||||
binding.textSettingName.alpha = opacity
|
binding.textSettingName.alpha = opacity
|
||||||
binding.textSettingDescription.alpha = opacity
|
binding.textSettingDescription.alpha = opacity
|
||||||
|
binding.buttonClear.isEnabled = isEditable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
|
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
|
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||||
SettingViewHolder(binding.root, adapter) {
|
SettingViewHolder(binding.root, adapter) {
|
||||||
@ -29,20 +30,31 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
|
|||||||
val resMgr = binding.textSettingValue.context.resources
|
val resMgr = binding.textSettingValue.context.resources
|
||||||
val values = resMgr.getIntArray(item.valuesId)
|
val values = resMgr.getIntArray(item.valuesId)
|
||||||
for (i in values.indices) {
|
for (i in values.indices) {
|
||||||
if (values[i] == item.selectedValue) {
|
if (values[i] == item.getSelectedValue()) {
|
||||||
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
|
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (item is StringSingleChoiceSetting) {
|
} else if (item is StringSingleChoiceSetting) {
|
||||||
for (i in item.values.indices) {
|
for (i in item.values.indices) {
|
||||||
if (item.values[i] == item.selectedValue) {
|
if (item.values[i] == item.getSelectedValue()) {
|
||||||
binding.textSettingValue.text = item.choices[i]
|
binding.textSettingValue.text = item.choices[i]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||||
|
!NativeConfig.isPerGameConfigLoaded()
|
||||||
|
) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
binding.buttonClear.setOnClickListener {
|
||||||
|
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
setStyle(setting.isEditable, binding)
|
setStyle(setting.isEditable, binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
|
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
||||||
SettingViewHolder(binding.root, adapter) {
|
SettingViewHolder(binding.root, adapter) {
|
||||||
@ -26,10 +27,21 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
|
|||||||
binding.textSettingValue.visibility = View.VISIBLE
|
binding.textSettingValue.visibility = View.VISIBLE
|
||||||
binding.textSettingValue.text = String.format(
|
binding.textSettingValue.text = String.format(
|
||||||
binding.textSettingValue.context.getString(R.string.value_with_units),
|
binding.textSettingValue.context.getString(R.string.value_with_units),
|
||||||
setting.selectedValue,
|
setting.getSelectedValue(),
|
||||||
setting.units
|
setting.units
|
||||||
)
|
)
|
||||||
|
|
||||||
|
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||||
|
!NativeConfig.isPerGameConfigLoaded()
|
||||||
|
) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
binding.buttonClear.setOnClickListener {
|
||||||
|
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
setStyle(setting.isEditable, binding)
|
setStyle(setting.isEditable, binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
|
|||||||
binding.textSettingDescription.visibility = View.GONE
|
binding.textSettingDescription.visibility = View.GONE
|
||||||
}
|
}
|
||||||
binding.textSettingValue.visibility = View.GONE
|
binding.textSettingValue.visibility = View.GONE
|
||||||
|
binding.buttonClear.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(clicked: View) {
|
override fun onClick(clicked: View) {
|
||||||
|
@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
|
import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
|
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
|
||||||
SettingViewHolder(binding.root, adapter) {
|
SettingViewHolder(binding.root, adapter) {
|
||||||
@ -27,9 +28,20 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.switchWidget.setOnCheckedChangeListener(null)
|
binding.switchWidget.setOnCheckedChangeListener(null)
|
||||||
binding.switchWidget.isChecked = setting.checked
|
binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
|
||||||
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||||
adapter.onBooleanClick(item, binding.switchWidget.isChecked)
|
adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonClear.visibility = if (setting.setting.global ||
|
||||||
|
!NativeConfig.isPerGameConfigLoaded()
|
||||||
|
) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
binding.buttonClear.setOnClickListener {
|
||||||
|
adapter.onClearClick(setting, bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle(setting.isEditable, binding)
|
setStyle(setting.isEditable, binding)
|
||||||
|
@ -3,15 +3,27 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.features.settings.utils
|
package org.yuzu.yuzu_emu.features.settings.utils
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains static methods for interacting with .ini files in which settings are stored.
|
* Contains static methods for interacting with .ini files in which settings are stored.
|
||||||
*/
|
*/
|
||||||
object SettingsFile {
|
object SettingsFile {
|
||||||
const val FILE_NAME_CONFIG = "config"
|
const val FILE_NAME_CONFIG = "config.ini"
|
||||||
|
|
||||||
fun getSettingsFile(fileName: String): File =
|
fun getSettingsFile(fileName: String): File =
|
||||||
File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")
|
File(DirectoryInitialization.userDirectory + "/config/" + fileName)
|
||||||
|
|
||||||
|
fun getCustomSettingsFile(game: Game): File =
|
||||||
|
File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini")
|
||||||
|
|
||||||
|
fun loadCustomConfig(game: Game) {
|
||||||
|
val fileName = FileUtil.getFilename(Uri.parse(game.path))
|
||||||
|
NativeConfig.initializePerGameConfig(game.programId, fileName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,10 @@ import org.yuzu.yuzu_emu.R
|
|||||||
import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
|
import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
|
||||||
import org.yuzu.yuzu_emu.model.GameDir
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
|
||||||
class AddGameFolderDialogFragment : DialogFragment() {
|
class AddGameFolderDialogFragment : DialogFragment() {
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@ -30,6 +32,7 @@ class AddGameFolderDialogFragment : DialogFragment() {
|
|||||||
.setTitle(R.string.add_game_folder)
|
.setTitle(R.string.add_game_folder)
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
|
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
|
||||||
|
homeViewModel.setGamesDirSelected(true)
|
||||||
gamesViewModel.addFolder(newGameDir)
|
gamesViewModel.addFolder(newGameDir)
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
@ -0,0 +1,214 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
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.LinearLayoutManager
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.adapters.AddonAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.AddonUtil
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AddonsFragment : Fragment() {
|
||||||
|
private var _binding: FragmentAddonsBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val addonViewModel: AddonViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<AddonsFragmentArgs>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
addonViewModel.onOpenAddons(args.game)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentAddonsBinding.inflate(inflater)
|
||||||
|
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 = false)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(false)
|
||||||
|
|
||||||
|
binding.toolbarAddons.setNavigationOnClickListener {
|
||||||
|
binding.root.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)
|
||||||
|
|
||||||
|
binding.listAddons.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
adapter = AddonAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
addonViewModel.addonList.collect {
|
||||||
|
(binding.listAddons.adapter as AddonAdapter).submitList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
addonViewModel.showModInstallPicker.collect {
|
||||||
|
if (it) {
|
||||||
|
installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||||
|
addonViewModel.showModInstallPicker(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
addonViewModel.showModNoticeDialog.collect {
|
||||||
|
if (it) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.addon_notice,
|
||||||
|
descriptionId = R.string.addon_notice_description,
|
||||||
|
positiveAction = { addonViewModel.showModInstallPicker(true) }
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
addonViewModel.showModNoticeDialog(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonInstall.setOnClickListener {
|
||||||
|
ContentTypeSelectionDialogFragment().show(
|
||||||
|
parentFragmentManager,
|
||||||
|
ContentTypeSelectionDialogFragment.TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
addonViewModel.refreshAddons()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
addonViewModel.onCloseAddons()
|
||||||
|
}
|
||||||
|
|
||||||
|
val installAddon =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result)
|
||||||
|
if (externalAddonDirectory == null) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.invalid_directory,
|
||||||
|
descriptionId = R.string.invalid_directory_description
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val isValid = externalAddonDirectory.listFiles()
|
||||||
|
.any { AddonUtil.validAddonDirectories.contains(it.name) }
|
||||||
|
val errorMessage = MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.invalid_directory,
|
||||||
|
descriptionId = R.string.invalid_directory_description
|
||||||
|
)
|
||||||
|
if (isValid) {
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
R.string.installing_game_content,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
val parentDirectoryName = externalAddonDirectory.name
|
||||||
|
val internalAddonDirectory =
|
||||||
|
File(args.game.addonDir + parentDirectoryName)
|
||||||
|
try {
|
||||||
|
externalAddonDirectory.copyFilesTo(internalAddonDirectory)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return@newInstance errorMessage
|
||||||
|
}
|
||||||
|
addonViewModel.refreshAddons()
|
||||||
|
return@newInstance getString(R.string.addon_installed_successfully)
|
||||||
|
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
} else {
|
||||||
|
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpToolbar.leftMargin = leftInsets
|
||||||
|
mlpToolbar.rightMargin = rightInsets
|
||||||
|
binding.toolbarAddons.layoutParams = mlpToolbar
|
||||||
|
|
||||||
|
val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpAddonsList.leftMargin = leftInsets
|
||||||
|
mlpAddonsList.rightMargin = rightInsets
|
||||||
|
binding.listAddons.layoutParams = mlpAddonsList
|
||||||
|
binding.listAddons.updatePadding(
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||||
|
)
|
||||||
|
|
||||||
|
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||||
|
val mlpFab =
|
||||||
|
binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||||
|
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||||
|
binding.buttonInstall.layoutParams = mlpFab
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
|
|
||||||
|
class ContentTypeSelectionDialogFragment : DialogFragment() {
|
||||||
|
private val addonViewModel: AddonViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val preferences get() =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
|
||||||
|
private var selectedItem = 0
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val launchOptions =
|
||||||
|
arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats))
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainActivity = requireActivity() as MainActivity
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.select_content_type)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
when (selectedItem) {
|
||||||
|
0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*"))
|
||||||
|
else -> {
|
||||||
|
if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) {
|
||||||
|
preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply()
|
||||||
|
addonViewModel.showModNoticeDialog(true)
|
||||||
|
return@setPositiveButton
|
||||||
|
}
|
||||||
|
addonViewModel.showModInstallPicker(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
|
||||||
|
selectedItem = i
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(SELECTED_ITEM, selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "ContentTypeSelectionDialogFragment"
|
||||||
|
|
||||||
|
private const val SELECTED_ITEM = "SelectedItem"
|
||||||
|
private const val MOD_NOTICE_SEEN = "ModNoticeSeen"
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@ -36,6 +37,8 @@ class DriverManagerFragment : Fragment() {
|
|||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
private val driverViewModel: DriverViewModel by activityViewModels()
|
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<DriverManagerFragmentArgs>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
@ -57,7 +60,9 @@ class DriverManagerFragment : Fragment() {
|
|||||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
if (!driverViewModel.isInteractionAllowed) {
|
driverViewModel.onOpenDriverManager(args.game)
|
||||||
|
|
||||||
|
if (!driverViewModel.isInteractionAllowed.value) {
|
||||||
DriversLoadingDialogFragment().show(
|
DriversLoadingDialogFragment().show(
|
||||||
childFragmentManager,
|
childFragmentManager,
|
||||||
DriversLoadingDialogFragment.TAG
|
DriversLoadingDialogFragment.TAG
|
||||||
@ -102,10 +107,9 @@ class DriverManagerFragment : Fragment() {
|
|||||||
setInsets()
|
setInsets()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start installing requested driver
|
override fun onDestroy() {
|
||||||
override fun onStop() {
|
super.onDestroy()
|
||||||
super.onStop()
|
driverViewModel.onCloseDriverManager(args.game)
|
||||||
driverViewModel.onCloseDriverManager()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setInsets() =
|
private fun setInsets() =
|
||||||
|
@ -47,25 +47,9 @@ class DriversLoadingDialogFragment : DialogFragment() {
|
|||||||
viewLifecycleOwner.lifecycleScope.apply {
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
launch {
|
launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
driverViewModel.areDriversLoading.collect { checkForDismiss() }
|
driverViewModel.isInteractionAllowed.collect { if (it) dismiss() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
|
||||||
driverViewModel.isDriverReady.collect { checkForDismiss() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
|
||||||
driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkForDismiss() {
|
|
||||||
if (driverViewModel.isInteractionAllowed) {
|
|
||||||
dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -33,7 +32,6 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import androidx.window.layout.FoldingFeature
|
import androidx.window.layout.FoldingFeature
|
||||||
import androidx.window.layout.WindowInfoTracker
|
import androidx.window.layout.WindowInfoTracker
|
||||||
import androidx.window.layout.WindowLayoutInfo
|
import androidx.window.layout.WindowLayoutInfo
|
||||||
@ -46,21 +44,22 @@ import kotlinx.coroutines.launch
|
|||||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||||
import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
|
import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
|
||||||
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
|
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
import org.yuzu.yuzu_emu.model.EmulationViewModel
|
import org.yuzu.yuzu_emu.model.EmulationViewModel
|
||||||
import org.yuzu.yuzu_emu.overlay.InputOverlay
|
import org.yuzu.yuzu_emu.overlay.model.OverlayControl
|
||||||
|
import org.yuzu.yuzu_emu.overlay.model.OverlayLayout
|
||||||
import org.yuzu.yuzu_emu.utils.*
|
import org.yuzu.yuzu_emu.utils.*
|
||||||
import java.lang.NullPointerException
|
import java.lang.NullPointerException
|
||||||
|
|
||||||
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
private lateinit var preferences: SharedPreferences
|
|
||||||
private lateinit var emulationState: EmulationState
|
private lateinit var emulationState: EmulationState
|
||||||
private var emulationActivity: EmulationActivity? = null
|
private var emulationActivity: EmulationActivity? = null
|
||||||
private var perfStatsUpdater: (() -> Unit)? = null
|
private var perfStatsUpdater: (() -> Unit)? = null
|
||||||
@ -127,9 +126,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always load custom settings when launching a game from an intent
|
||||||
|
if (args.custom || intentGame != null) {
|
||||||
|
SettingsFile.loadCustomConfig(game)
|
||||||
|
NativeConfig.unloadPerGameConfig()
|
||||||
|
} else {
|
||||||
|
NativeConfig.reloadGlobalConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the selected driver asynchronously as the game starts
|
||||||
|
driverViewModel.onLaunchGame()
|
||||||
|
|
||||||
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
// So this fragment doesn't restart on configuration changes; i.e. rotation.
|
||||||
retainInstance = true
|
retainInstance = true
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
emulationState = EmulationState(game.path)
|
emulationState = EmulationState(game.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,6 +226,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.menu_settings_per_game -> {
|
||||||
|
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||||
|
args.game,
|
||||||
|
Settings.MenuTag.SECTION_ROOT
|
||||||
|
)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.menu_overlay_controls -> {
|
R.id.menu_overlay_controls -> {
|
||||||
showOverlayOptions()
|
showOverlayOptions()
|
||||||
true
|
true
|
||||||
@ -332,8 +350,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
driverViewModel.isDriverReady.collect {
|
driverViewModel.isInteractionAllowed.collect {
|
||||||
if (it && !emulationState.isRunning) {
|
if (it) {
|
||||||
|
onEmulationStart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onEmulationStart() {
|
||||||
|
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
|
||||||
if (!DirectoryInitialization.areDirectoriesReady) {
|
if (!DirectoryInitialization.areDirectoriesReady) {
|
||||||
DirectoryInitialization.start()
|
DirectoryInitialization.start()
|
||||||
}
|
}
|
||||||
@ -343,10 +371,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
emulationState.run(emulationActivity!!.isActivityRecreated)
|
emulationState.run(emulationActivity!!.isActivityRecreated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
@ -355,24 +379,25 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateScreenLayout()
|
updateScreenLayout()
|
||||||
|
val showInputOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
|
||||||
if (emulationActivity?.isInPictureInPictureMode == true) {
|
if (emulationActivity?.isInPictureInPictureMode == true) {
|
||||||
if (binding.drawerLayout.isOpen) {
|
if (binding.drawerLayout.isOpen) {
|
||||||
binding.drawerLayout.close()
|
binding.drawerLayout.close()
|
||||||
}
|
}
|
||||||
if (EmulationMenuSettings.showOverlay) {
|
if (showInputOverlay) {
|
||||||
binding.surfaceInputOverlay.visibility = View.INVISIBLE
|
binding.surfaceInputOverlay.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) {
|
if (showInputOverlay && emulationViewModel.emulationStarted.value) {
|
||||||
binding.surfaceInputOverlay.visibility = View.VISIBLE
|
binding.surfaceInputOverlay.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.surfaceInputOverlay.visibility = View.INVISIBLE
|
binding.surfaceInputOverlay.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (!isInFoldableLayout) {
|
if (!isInFoldableLayout) {
|
||||||
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
binding.surfaceInputOverlay.layout = InputOverlay.PORTRAIT
|
binding.surfaceInputOverlay.layout = OverlayLayout.Portrait
|
||||||
} else {
|
} else {
|
||||||
binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE
|
binding.surfaceInputOverlay.layout = OverlayLayout.Landscape
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -396,17 +421,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun resetInputOverlay() {
|
private fun resetInputOverlay() {
|
||||||
preferences.edit()
|
IntSetting.OVERLAY_SCALE.reset()
|
||||||
.remove(Settings.PREF_CONTROL_SCALE)
|
IntSetting.OVERLAY_OPACITY.reset()
|
||||||
.remove(Settings.PREF_CONTROL_OPACITY)
|
|
||||||
.apply()
|
|
||||||
binding.surfaceInputOverlay.post {
|
binding.surfaceInputOverlay.post {
|
||||||
binding.surfaceInputOverlay.resetLayoutVisibilityAndPlacement()
|
binding.surfaceInputOverlay.resetLayoutVisibilityAndPlacement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateShowFpsOverlay() {
|
private fun updateShowFpsOverlay() {
|
||||||
if (EmulationMenuSettings.showFps) {
|
if (BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()) {
|
||||||
val SYSTEM_FPS = 0
|
val SYSTEM_FPS = 0
|
||||||
val FPS = 1
|
val FPS = 1
|
||||||
val FRAMETIME = 2
|
val FRAMETIME = 2
|
||||||
@ -435,7 +458,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
@SuppressLint("SourceLockedOrientationActivity")
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
private fun updateOrientation() {
|
private fun updateOrientation() {
|
||||||
emulationActivity?.let {
|
emulationActivity?.let {
|
||||||
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
|
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) {
|
||||||
Settings.LayoutOption_MobileLandscape ->
|
Settings.LayoutOption_MobileLandscape ->
|
||||||
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||||
Settings.LayoutOption_MobilePortrait ->
|
Settings.LayoutOption_MobilePortrait ->
|
||||||
@ -469,7 +492,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
binding.inGameMenu.layoutParams.height = it.bounds.bottom
|
binding.inGameMenu.layoutParams.height = it.bounds.bottom
|
||||||
|
|
||||||
isInFoldableLayout = true
|
isInFoldableLayout = true
|
||||||
binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE
|
binding.surfaceInputOverlay.layout = OverlayLayout.Foldable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.isSeparating
|
it.isSeparating
|
||||||
@ -508,18 +531,22 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
popup.menuInflater.inflate(R.menu.menu_overlay_options, popup.menu)
|
popup.menuInflater.inflate(R.menu.menu_overlay_options, popup.menu)
|
||||||
|
|
||||||
popup.menu.apply {
|
popup.menu.apply {
|
||||||
findItem(R.id.menu_toggle_fps).isChecked = EmulationMenuSettings.showFps
|
findItem(R.id.menu_toggle_fps).isChecked =
|
||||||
findItem(R.id.menu_rel_stick_center).isChecked = EmulationMenuSettings.joystickRelCenter
|
BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()
|
||||||
findItem(R.id.menu_dpad_slide).isChecked = EmulationMenuSettings.dpadSlide
|
findItem(R.id.menu_rel_stick_center).isChecked =
|
||||||
findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
|
BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()
|
||||||
findItem(R.id.menu_haptics).isChecked = EmulationMenuSettings.hapticFeedback
|
findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean()
|
||||||
|
findItem(R.id.menu_show_overlay).isChecked =
|
||||||
|
BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
|
||||||
|
findItem(R.id.menu_haptics).isChecked = BooleanSetting.HAPTIC_FEEDBACK.getBoolean()
|
||||||
|
findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean()
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener {
|
popup.setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.menu_toggle_fps -> {
|
R.id.menu_toggle_fps -> {
|
||||||
it.isChecked = !it.isChecked
|
it.isChecked = !it.isChecked
|
||||||
EmulationMenuSettings.showFps = it.isChecked
|
BooleanSetting.SHOW_PERFORMANCE_OVERLAY.setBoolean(it.isChecked)
|
||||||
updateShowFpsOverlay()
|
updateShowFpsOverlay()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -537,11 +564,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_toggle_controls -> {
|
R.id.menu_toggle_controls -> {
|
||||||
val preferences =
|
val overlayControlData = NativeConfig.getOverlayControlData()
|
||||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
val optionsArray = BooleanArray(overlayControlData.size)
|
||||||
val optionsArray = BooleanArray(Settings.overlayPreferences.size)
|
overlayControlData.forEachIndexed { i, _ ->
|
||||||
Settings.overlayPreferences.forEachIndexed { i, _ ->
|
optionsArray[i] = overlayControlData.firstOrNull { data ->
|
||||||
optionsArray[i] = preferences.getBoolean("buttonToggle$i", i < 15)
|
OverlayControl.entries[i].id == data.id
|
||||||
|
}?.enabled == true
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
@ -550,11 +578,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
R.array.gamepadButtons,
|
R.array.gamepadButtons,
|
||||||
optionsArray
|
optionsArray
|
||||||
) { _, indexSelected, isChecked ->
|
) { _, indexSelected, isChecked ->
|
||||||
preferences.edit()
|
overlayControlData.firstOrNull { data ->
|
||||||
.putBoolean("buttonToggle$indexSelected", isChecked)
|
OverlayControl.entries[indexSelected].id == data.id
|
||||||
.apply()
|
}?.enabled = isChecked
|
||||||
}
|
}
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
NativeConfig.setOverlayControlData(overlayControlData)
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
binding.surfaceInputOverlay.refreshControls()
|
binding.surfaceInputOverlay.refreshControls()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
@ -565,12 +595,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
|
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
val isChecked = !optionsArray[0]
|
val isChecked = !optionsArray[0]
|
||||||
Settings.overlayPreferences.forEachIndexed { i, _ ->
|
overlayControlData.forEachIndexed { i, _ ->
|
||||||
optionsArray[i] = isChecked
|
optionsArray[i] = isChecked
|
||||||
dialog.listView.setItemChecked(i, isChecked)
|
dialog.listView.setItemChecked(i, isChecked)
|
||||||
preferences.edit()
|
overlayControlData[i].enabled = isChecked
|
||||||
.putBoolean("buttonToggle$i", isChecked)
|
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@ -578,26 +606,32 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
|
|
||||||
R.id.menu_show_overlay -> {
|
R.id.menu_show_overlay -> {
|
||||||
it.isChecked = !it.isChecked
|
it.isChecked = !it.isChecked
|
||||||
EmulationMenuSettings.showOverlay = it.isChecked
|
BooleanSetting.SHOW_INPUT_OVERLAY.setBoolean(it.isChecked)
|
||||||
binding.surfaceInputOverlay.refreshControls()
|
binding.surfaceInputOverlay.refreshControls()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_rel_stick_center -> {
|
R.id.menu_rel_stick_center -> {
|
||||||
it.isChecked = !it.isChecked
|
it.isChecked = !it.isChecked
|
||||||
EmulationMenuSettings.joystickRelCenter = it.isChecked
|
BooleanSetting.JOYSTICK_REL_CENTER.setBoolean(it.isChecked)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_dpad_slide -> {
|
R.id.menu_dpad_slide -> {
|
||||||
it.isChecked = !it.isChecked
|
it.isChecked = !it.isChecked
|
||||||
EmulationMenuSettings.dpadSlide = it.isChecked
|
BooleanSetting.DPAD_SLIDE.setBoolean(it.isChecked)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_haptics -> {
|
R.id.menu_haptics -> {
|
||||||
it.isChecked = !it.isChecked
|
it.isChecked = !it.isChecked
|
||||||
EmulationMenuSettings.hapticFeedback = it.isChecked
|
BooleanSetting.HAPTIC_FEEDBACK.setBoolean(it.isChecked)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_touchscreen -> {
|
||||||
|
it.isChecked = !it.isChecked
|
||||||
|
BooleanSetting.TOUCHSCREEN.setBoolean(it.isChecked)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,7 +651,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
@SuppressLint("SourceLockedOrientationActivity")
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
private fun startConfiguringControls() {
|
private fun startConfiguringControls() {
|
||||||
// Lock the current orientation to prevent editing inconsistencies
|
// Lock the current orientation to prevent editing inconsistencies
|
||||||
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
|
if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
|
||||||
emulationActivity?.let {
|
emulationActivity?.let {
|
||||||
it.requestedOrientation =
|
it.requestedOrientation =
|
||||||
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
@ -635,11 +669,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
binding.doneControlConfig.visibility = View.GONE
|
binding.doneControlConfig.visibility = View.GONE
|
||||||
binding.surfaceInputOverlay.setIsInEditMode(false)
|
binding.surfaceInputOverlay.setIsInEditMode(false)
|
||||||
// Unlock the orientation if it was locked for editing
|
// Unlock the orientation if it was locked for editing
|
||||||
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
|
if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
|
||||||
emulationActivity?.let {
|
emulationActivity?.let {
|
||||||
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
@ -648,7 +683,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
adjustBinding.apply {
|
adjustBinding.apply {
|
||||||
inputScaleSlider.apply {
|
inputScaleSlider.apply {
|
||||||
valueTo = 150F
|
valueTo = 150F
|
||||||
value = preferences.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
|
value = IntSetting.OVERLAY_SCALE.getInt().toFloat()
|
||||||
addOnChangeListener(
|
addOnChangeListener(
|
||||||
Slider.OnChangeListener { _, value, _ ->
|
Slider.OnChangeListener { _, value, _ ->
|
||||||
inputScaleValue.text = "${value.toInt()}%"
|
inputScaleValue.text = "${value.toInt()}%"
|
||||||
@ -658,7 +693,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
inputOpacitySlider.apply {
|
inputOpacitySlider.apply {
|
||||||
valueTo = 100F
|
valueTo = 100F
|
||||||
value = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100).toFloat()
|
value = IntSetting.OVERLAY_OPACITY.getInt().toFloat()
|
||||||
addOnChangeListener(
|
addOnChangeListener(
|
||||||
Slider.OnChangeListener { _, value, _ ->
|
Slider.OnChangeListener { _, value, _ ->
|
||||||
inputOpacityValue.text = "${value.toInt()}%"
|
inputOpacityValue.text = "${value.toInt()}%"
|
||||||
@ -682,16 +717,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setControlScale(scale: Int) {
|
private fun setControlScale(scale: Int) {
|
||||||
preferences.edit()
|
IntSetting.OVERLAY_SCALE.setInt(scale)
|
||||||
.putInt(Settings.PREF_CONTROL_SCALE, scale)
|
|
||||||
.apply()
|
|
||||||
binding.surfaceInputOverlay.refreshControls()
|
binding.surfaceInputOverlay.refreshControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setControlOpacity(opacity: Int) {
|
private fun setControlOpacity(opacity: Int) {
|
||||||
preferences.edit()
|
IntSetting.OVERLAY_OPACITY.setInt(opacity)
|
||||||
.putInt(Settings.PREF_CONTROL_OPACITY, opacity)
|
|
||||||
.apply()
|
|
||||||
binding.surfaceInputOverlay.refreshControls()
|
binding.surfaceInputOverlay.refreshControls()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R
|
|||||||
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||||
import org.yuzu.yuzu_emu.model.GameDir
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
||||||
|
|
||||||
class GameFolderPropertiesDialogFragment : DialogFragment() {
|
class GameFolderPropertiesDialogFragment : DialogFragment() {
|
||||||
@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putBoolean(DEEP_SCAN, deepScan)
|
outState.putBoolean(DEEP_SCAN, deepScan)
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
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.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||||
|
|
||||||
|
class GameInfoFragment : Fragment() {
|
||||||
|
private var _binding: FragmentGameInfoBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<GameInfoFragmentArgs>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
|
||||||
|
// Check for an up-to-date version string
|
||||||
|
args.game.version = GameMetadata.getVersion(args.game.path, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentGameInfoBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(false)
|
||||||
|
|
||||||
|
binding.apply {
|
||||||
|
toolbarInfo.title = args.game.title
|
||||||
|
toolbarInfo.setNavigationOnClickListener {
|
||||||
|
view.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val pathString = Uri.parse(args.game.path).path ?: ""
|
||||||
|
path.setHint(R.string.path)
|
||||||
|
pathField.setText(pathString)
|
||||||
|
pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) }
|
||||||
|
|
||||||
|
programId.setHint(R.string.program_id)
|
||||||
|
programIdField.setText(args.game.programIdHex)
|
||||||
|
programIdField.setOnClickListener {
|
||||||
|
copyToClipboard(getString(R.string.program_id), args.game.programIdHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.game.developer.isNotEmpty()) {
|
||||||
|
developer.setHint(R.string.developer)
|
||||||
|
developerField.setText(args.game.developer)
|
||||||
|
developerField.setOnClickListener {
|
||||||
|
copyToClipboard(getString(R.string.developer), args.game.developer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
developer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
version.setHint(R.string.version)
|
||||||
|
versionField.setText(args.game.version)
|
||||||
|
versionField.setOnClickListener {
|
||||||
|
copyToClipboard(getString(R.string.version), args.game.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonCopy.setOnClickListener {
|
||||||
|
val details = """
|
||||||
|
${args.game.title}
|
||||||
|
${getString(R.string.path)} - $pathString
|
||||||
|
${getString(R.string.program_id)} - ${args.game.programIdHex}
|
||||||
|
${getString(R.string.developer)} - ${args.game.developer}
|
||||||
|
${getString(R.string.version)} - ${args.game.version}
|
||||||
|
""".trimIndent()
|
||||||
|
copyToClipboard(args.game.title, details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyToClipboard(label: String, body: String) {
|
||||||
|
val clipBoard =
|
||||||
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText(label, body)
|
||||||
|
clipBoard.setPrimaryClip(clip)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.copied_to_clipboard,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val mlpToolbar = binding.toolbarInfo.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpToolbar.leftMargin = leftInsets
|
||||||
|
mlpToolbar.rightMargin = rightInsets
|
||||||
|
binding.toolbarInfo.layoutParams = mlpToolbar
|
||||||
|
|
||||||
|
val mlpScrollAbout = binding.scrollInfo.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpScrollAbout.leftMargin = leftInsets
|
||||||
|
mlpScrollAbout.rightMargin = rightInsets
|
||||||
|
binding.scrollInfo.layoutParams = mlpScrollAbout
|
||||||
|
|
||||||
|
binding.contentInfo.updatePadding(bottom = barInsets.bottom)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,456 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
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.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.adapters.GamePropertiesAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
|
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.GameProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.InstallableProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.SubmenuProperty
|
||||||
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
|
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
|
||||||
|
|
||||||
|
class GamePropertiesFragment : Fragment() {
|
||||||
|
private var _binding: FragmentGamePropertiesBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
private val driverViewModel: DriverViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private val args by navArgs<GamePropertiesFragmentArgs>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentGamePropertiesBinding.inflate(layoutInflater)
|
||||||
|
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(true)
|
||||||
|
|
||||||
|
binding.buttonBack.setOnClickListener {
|
||||||
|
view.findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
|
||||||
|
binding.title.text = args.game.title
|
||||||
|
binding.title.postDelayed(
|
||||||
|
{
|
||||||
|
binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||||
|
binding.title.isSelected = true
|
||||||
|
},
|
||||||
|
3000
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.buttonStart.setOnClickListener {
|
||||||
|
LaunchGameDialogFragment.newInstance(args.game)
|
||||||
|
.show(childFragmentManager, LaunchGameDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadList()
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
homeViewModel.openImportSaves.collect {
|
||||||
|
if (it) {
|
||||||
|
importSaves.launch(arrayOf("application/zip"))
|
||||||
|
homeViewModel.setOpenImportSaves(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
homeViewModel.reloadPropertiesList.collect {
|
||||||
|
if (it) {
|
||||||
|
reloadList()
|
||||||
|
homeViewModel.reloadPropertiesList(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
gamesViewModel.reloadGames(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reloadList() {
|
||||||
|
_binding ?: return
|
||||||
|
|
||||||
|
driverViewModel.updateDriverNameForGame(args.game)
|
||||||
|
val properties = mutableListOf<GameProperty>().apply {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.info,
|
||||||
|
R.string.info_description,
|
||||||
|
R.drawable.ic_info_outline
|
||||||
|
) {
|
||||||
|
val action = GamePropertiesFragmentDirections
|
||||||
|
.actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.preferences_settings,
|
||||||
|
R.string.per_game_settings_description,
|
||||||
|
R.drawable.ic_settings
|
||||||
|
) {
|
||||||
|
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||||
|
args.game,
|
||||||
|
Settings.MenuTag.SECTION_ROOT
|
||||||
|
)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (GpuDriverHelper.supportsCustomDriverLoading()) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.gpu_driver_manager,
|
||||||
|
R.string.install_gpu_driver_description,
|
||||||
|
R.drawable.ic_build,
|
||||||
|
detailsFlow = driverViewModel.selectedDriverTitle
|
||||||
|
) {
|
||||||
|
val action = GamePropertiesFragmentDirections
|
||||||
|
.actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.game.isHomebrew) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.add_ons,
|
||||||
|
R.string.add_ons_description,
|
||||||
|
R.drawable.ic_edit
|
||||||
|
) {
|
||||||
|
val action = GamePropertiesFragmentDirections
|
||||||
|
.actionPerGamePropertiesFragmentToAddonsFragment(args.game)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
InstallableProperty(
|
||||||
|
R.string.save_data,
|
||||||
|
R.string.save_data_description,
|
||||||
|
R.drawable.ic_save,
|
||||||
|
{
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.import_save_warning,
|
||||||
|
descriptionId = R.string.import_save_warning_description,
|
||||||
|
positiveAction = { homeViewModel.setOpenImportSaves(true) }
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
},
|
||||||
|
if (File(args.game.saveDir).exists()) {
|
||||||
|
{ exportSaves.launch(args.game.saveZipName) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val saveDirFile = File(args.game.saveDir)
|
||||||
|
if (saveDirFile.exists()) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.delete_save_data,
|
||||||
|
R.string.delete_save_data_description,
|
||||||
|
R.drawable.ic_delete,
|
||||||
|
action = {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.delete_save_data,
|
||||||
|
descriptionId = R.string.delete_save_data_warning_description,
|
||||||
|
positiveAction = {
|
||||||
|
File(args.game.saveDir).deleteRecursively()
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
R.string.save_data_deleted_successfully,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
homeViewModel.reloadPropertiesList(true)
|
||||||
|
}
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shaderCacheDir = File(
|
||||||
|
DirectoryInitialization.userDirectory +
|
||||||
|
"/shader/" + args.game.settingsName.lowercase()
|
||||||
|
)
|
||||||
|
if (shaderCacheDir.exists()) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.clear_shader_cache,
|
||||||
|
R.string.clear_shader_cache_description,
|
||||||
|
R.drawable.ic_delete,
|
||||||
|
{
|
||||||
|
if (shaderCacheDir.exists()) {
|
||||||
|
val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
|
||||||
|
.map { it.length() }.sum()
|
||||||
|
MemoryUtil.bytesToSizeUnit(bytes.toFloat())
|
||||||
|
} else {
|
||||||
|
MemoryUtil.bytesToSizeUnit(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.clear_shader_cache,
|
||||||
|
descriptionId = R.string.clear_shader_cache_warning_description,
|
||||||
|
positiveAction = {
|
||||||
|
shaderCacheDir.deleteRecursively()
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
R.string.cleared_shaders_successfully,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
homeViewModel.reloadPropertiesList(true)
|
||||||
|
}
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.listProperties.apply {
|
||||||
|
layoutManager =
|
||||||
|
GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
|
||||||
|
adapter = GamePropertiesAdapter(viewLifecycleOwner, properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
driverViewModel.updateDriverNameForGame(args.game)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val smallLayout = resources.getBoolean(R.bool.small_layout)
|
||||||
|
if (smallLayout) {
|
||||||
|
val mlpListAll =
|
||||||
|
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListAll.leftMargin = leftInsets
|
||||||
|
mlpListAll.rightMargin = rightInsets
|
||||||
|
binding.listAll.layoutParams = mlpListAll
|
||||||
|
} else {
|
||||||
|
if (ViewCompat.getLayoutDirection(binding.root) ==
|
||||||
|
ViewCompat.LAYOUT_DIRECTION_LTR
|
||||||
|
) {
|
||||||
|
val mlpListAll =
|
||||||
|
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListAll.rightMargin = rightInsets
|
||||||
|
binding.listAll.layoutParams = mlpListAll
|
||||||
|
|
||||||
|
val mlpIconLayout =
|
||||||
|
binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpIconLayout.topMargin = barInsets.top
|
||||||
|
mlpIconLayout.leftMargin = leftInsets
|
||||||
|
binding.iconLayout!!.layoutParams = mlpIconLayout
|
||||||
|
} else {
|
||||||
|
val mlpListAll =
|
||||||
|
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpListAll.leftMargin = leftInsets
|
||||||
|
binding.listAll.layoutParams = mlpListAll
|
||||||
|
|
||||||
|
val mlpIconLayout =
|
||||||
|
binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpIconLayout.topMargin = barInsets.top
|
||||||
|
mlpIconLayout.rightMargin = rightInsets
|
||||||
|
binding.iconLayout!!.layoutParams = mlpIconLayout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||||
|
val mlpFab =
|
||||||
|
binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||||
|
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||||
|
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||||
|
binding.buttonStart.layoutParams = mlpFab
|
||||||
|
|
||||||
|
binding.layoutAll.updatePadding(
|
||||||
|
top = barInsets.top,
|
||||||
|
bottom = barInsets.bottom +
|
||||||
|
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
private val importSaves =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
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(
|
||||||
|
requireActivity(),
|
||||||
|
R.string.save_files_importing,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
||||||
|
val files = cacheSaveDir.listFiles()
|
||||||
|
var savesFolderFile: File? = null
|
||||||
|
if (files != null) {
|
||||||
|
val savesFolderName = args.game.programIdHex
|
||||||
|
for (file in files) {
|
||||||
|
if (file.isDirectory && file.name == savesFolderName) {
|
||||||
|
savesFolderFile = file
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savesFolderFile != null) {
|
||||||
|
savesFolder.deleteRecursively()
|
||||||
|
savesFolder.mkdir()
|
||||||
|
savesFolderFile.copyRecursively(savesFolder)
|
||||||
|
savesFolderFile.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (savesFolderFile == null) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.save_file_invalid_zip_structure,
|
||||||
|
descriptionId = R.string.save_file_invalid_zip_structure_description
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
getString(R.string.save_file_imported_success),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
homeViewModel.reloadPropertiesList(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheSaveDir.deleteRecursively()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
getString(R.string.fatal_error),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the save file located in the given folder path by creating a zip file and opening a
|
||||||
|
* file picker to save.
|
||||||
|
*/
|
||||||
|
private val exportSaves = registerForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/zip")
|
||||||
|
) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
R.string.save_files_exporting,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
val saveLocation = args.game.saveDir
|
||||||
|
val zipResult = FileUtil.zipFromInternalStorage(
|
||||||
|
File(saveLocation),
|
||||||
|
saveLocation.replaceAfterLast("/", ""),
|
||||||
|
BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
|
||||||
|
)
|
||||||
|
return@newInstance when (zipResult) {
|
||||||
|
TaskState.Completed -> getString(R.string.export_success)
|
||||||
|
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
||||||
|
}
|
||||||
|
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
}
|
@ -68,6 +68,9 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||||
mainActivity = requireActivity() as MainActivity
|
mainActivity = requireActivity() as MainActivity
|
||||||
|
|
||||||
val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
|
val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
|
||||||
@ -91,13 +94,14 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
R.string.install_gpu_driver_description,
|
R.string.install_gpu_driver_description,
|
||||||
R.drawable.ic_build,
|
R.drawable.ic_build,
|
||||||
{
|
{
|
||||||
binding.root.findNavController()
|
val action = HomeSettingsFragmentDirections
|
||||||
.navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
|
.actionHomeSettingsFragmentToDriverManagerFragment(null)
|
||||||
|
binding.root.findNavController().navigate(action)
|
||||||
},
|
},
|
||||||
{ GpuDriverHelper.supportsCustomDriverLoading() },
|
{ GpuDriverHelper.supportsCustomDriverLoading() },
|
||||||
R.string.custom_driver_not_supported,
|
R.string.custom_driver_not_supported,
|
||||||
R.string.custom_driver_not_supported_description,
|
R.string.custom_driver_not_supported_description,
|
||||||
driverViewModel.selectedDriverMetadata
|
driverViewModel.selectedDriverTitle
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
@ -212,8 +216,11 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
exitTransition = null
|
exitTransition = null
|
||||||
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
}
|
||||||
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
driverViewModel.updateDriverNameForGame(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
|||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
titleId: Int,
|
titleId: Int,
|
||||||
cancellable: Boolean = false,
|
cancellable: Boolean = false,
|
||||||
task: () -> Any
|
task: suspend () -> Any
|
||||||
): IndeterminateProgressDialogFragment {
|
): IndeterminateProgressDialogFragment {
|
||||||
val dialog = IndeterminateProgressDialogFragment()
|
val dialog = IndeterminateProgressDialogFragment()
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
|
@ -21,8 +21,6 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
|
|||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
import org.yuzu.yuzu_emu.model.Installable
|
import org.yuzu.yuzu_emu.model.Installable
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
class InstallableFragment : Fragment() {
|
class InstallableFragment : Fragment() {
|
||||||
private var _binding: FragmentInstallablesBinding? = null
|
private var _binding: FragmentInstallablesBinding? = null
|
||||||
@ -75,28 +73,6 @@ class InstallableFragment : Fragment() {
|
|||||||
R.string.install_firmware_description,
|
R.string.install_firmware_description,
|
||||||
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
|
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
|
||||||
),
|
),
|
||||||
if (mainActivity.savesFolderRoot != "") {
|
|
||||||
Installable(
|
|
||||||
R.string.manage_save_data,
|
|
||||||
R.string.import_export_saves_description,
|
|
||||||
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) },
|
|
||||||
export = {
|
|
||||||
mainActivity.exportSaves.launch(
|
|
||||||
"yuzu saves - ${
|
|
||||||
LocalDateTime.now().format(
|
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
|
||||||
)
|
|
||||||
}.zip"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Installable(
|
|
||||||
R.string.manage_save_data,
|
|
||||||
R.string.import_export_saves_description,
|
|
||||||
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
Installable(
|
Installable(
|
||||||
R.string.install_prod_keys,
|
R.string.install_prod_keys,
|
||||||
R.string.install_prod_keys_description,
|
R.string.install_prod_keys_description,
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
||||||
|
|
||||||
|
class LaunchGameDialogFragment : DialogFragment() {
|
||||||
|
private var selectedItem = 1
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val game = requireArguments().parcelable<Game>(GAME)
|
||||||
|
val launchOptions = arrayOf(getString(R.string.global), getString(R.string.custom))
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.launch_options)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
val action = HomeNavigationDirections
|
||||||
|
.actionGlobalEmulationActivity(game, selectedItem != 0)
|
||||||
|
requireParentFragment().findNavController().navigate(action)
|
||||||
|
}
|
||||||
|
.setSingleChoiceItems(launchOptions, 1) { _: DialogInterface, i: Int ->
|
||||||
|
selectedItem = i
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(SELECTED_ITEM, selectedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LaunchGameDialogFragment"
|
||||||
|
|
||||||
|
const val GAME = "Game"
|
||||||
|
const val SELECTED_ITEM = "SelectedItem"
|
||||||
|
|
||||||
|
fun newInstance(game: Game): LaunchGameDialogFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putParcelable(GAME, game)
|
||||||
|
val fragment = LaunchGameDialogFragment()
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() {
|
|||||||
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
|
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
|
||||||
val helpLinkId = requireArguments().getInt(HELP_LINK)
|
val helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setPositiveButton(R.string.close, null)
|
|
||||||
|
|
||||||
if (titleId != 0) dialog.setTitle(titleId)
|
if (messageDialogViewModel.positiveAction == null) {
|
||||||
if (titleString.isNotEmpty()) dialog.setTitle(titleString)
|
builder.setPositiveButton(R.string.close, null)
|
||||||
|
} else {
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
|
messageDialogViewModel.positiveAction?.invoke()
|
||||||
|
}.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleId != 0) builder.setTitle(titleId)
|
||||||
|
if (titleString.isNotEmpty()) builder.setTitle(titleString)
|
||||||
|
|
||||||
if (descriptionId != 0) {
|
if (descriptionId != 0) {
|
||||||
dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
|
builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
|
||||||
}
|
}
|
||||||
if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)
|
if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString)
|
||||||
|
|
||||||
if (helpLinkId != 0) {
|
if (helpLinkId != 0) {
|
||||||
dialog.setNeutralButton(R.string.learn_more) { _, _ ->
|
builder.setNeutralButton(R.string.learn_more) { _, _ ->
|
||||||
openLink(getString(helpLinkId))
|
openLink(getString(helpLinkId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialog.show()
|
return builder.show()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
messageDialogViewModel.dismissAction.invoke()
|
|
||||||
messageDialogViewModel.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openLink(link: String) {
|
private fun openLink(link: String) {
|
||||||
@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() {
|
|||||||
descriptionId: Int = 0,
|
descriptionId: Int = 0,
|
||||||
descriptionString: String = "",
|
descriptionString: String = "",
|
||||||
helpLinkId: Int = 0,
|
helpLinkId: Int = 0,
|
||||||
dismissAction: () -> Unit = {}
|
positiveAction: (() -> Unit)? = null
|
||||||
): MessageDialogFragment {
|
): MessageDialogFragment {
|
||||||
val dialog = MessageDialogFragment()
|
val dialog = MessageDialogFragment()
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() {
|
|||||||
putString(DESCRIPTION_STRING, descriptionString)
|
putString(DESCRIPTION_STRING, descriptionString)
|
||||||
putInt(HELP_LINK, helpLinkId)
|
putInt(HELP_LINK, helpLinkId)
|
||||||
}
|
}
|
||||||
ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
|
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
|
||||||
dismissAction
|
clear()
|
||||||
|
this.positiveAction = positiveAction
|
||||||
|
}
|
||||||
dialog.arguments = bundle
|
dialog.arguments = bundle
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import info.debatty.java.stringsimilarity.Jaccard
|
import info.debatty.java.stringsimilarity.Jaccard
|
||||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
@ -60,7 +61,9 @@ class SearchFragment : Fragment() {
|
|||||||
// This is using the correct scope, lint is just acting up
|
// This is using the correct scope, lint is just acting up
|
||||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
homeViewModel.setNavigationVisibility(visible = true, animated = false)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(true)
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
@ -99,7 +102,7 @@ class SearchFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
gamesViewModel.games.collect { filterAndSearch() }
|
gamesViewModel.games.collectLatest { filterAndSearch() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
|
@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
|||||||
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
|
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
|
||||||
val item = settingsViewModel.clickedItem as SliderSetting
|
val item = settingsViewModel.clickedItem as SliderSetting
|
||||||
|
|
||||||
settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units)
|
settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
|
||||||
sliderBinding.slider.apply {
|
sliderBinding.slider.apply {
|
||||||
valueFrom = item.min.toFloat()
|
valueFrom = item.min.toFloat()
|
||||||
valueTo = item.max.toFloat()
|
valueTo = item.max.toFloat()
|
||||||
@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
|||||||
is SingleChoiceSetting -> {
|
is SingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||||
scSetting.selectedValue = value
|
scSetting.setSelectedValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
is StringSingleChoiceSetting -> {
|
is StringSingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
||||||
val value = scSetting.getValueAt(which)
|
val value = scSetting.getValueAt(which)
|
||||||
scSetting.selectedValue = value
|
scSetting.setSelectedValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
is SliderSetting -> {
|
is SliderSetting -> {
|
||||||
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
||||||
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
closeDialog()
|
closeDialog()
|
||||||
@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
|
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
|
||||||
val value = item.selectedValue
|
val value = item.getSelectedValue()
|
||||||
val valuesId = item.valuesId
|
val valuesId = item.valuesId
|
||||||
if (valuesId > 0) {
|
if (valuesId > 0) {
|
||||||
val valuesArray = requireContext().resources.getIntArray(valuesId)
|
val valuesArray = requireContext().resources.getIntArray(valuesId)
|
||||||
@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
|||||||
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
|
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
|
||||||
|
|
||||||
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
|
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
|
||||||
(clickedItem as SliderSetting).selectedValue.toFloat()
|
(clickedItem as SliderSetting).getSelectedValue().toFloat()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
settingsViewModel.clickedItem = clickedItem
|
settingsViewModel.clickedItem = clickedItem
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -75,6 +76,8 @@ class SetupFragment : Fragment() {
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is using the correct scope, lint is just acting up
|
||||||
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
mainActivity = requireActivity() as MainActivity
|
mainActivity = requireActivity() as MainActivity
|
||||||
|
|
||||||
@ -206,7 +209,8 @@ class SetupFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
|
launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
homeViewModel.shouldPageForward.collect {
|
homeViewModel.shouldPageForward.collect {
|
||||||
if (it) {
|
if (it) {
|
||||||
@ -216,6 +220,17 @@ class SetupFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.gamesDirSelected.collect {
|
||||||
|
if (it) {
|
||||||
|
gamesDirCallback.onStepCompleted()
|
||||||
|
homeViewModel.setGamesDirSelected(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.viewPager2.apply {
|
binding.viewPager2.apply {
|
||||||
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
|
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
|
||||||
@ -289,6 +304,11 @@ class SetupFragment : Fragment() {
|
|||||||
setInsets()
|
setInsets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
if (_binding != null) {
|
if (_binding != null) {
|
||||||
@ -339,7 +359,6 @@ class SetupFragment : Fragment() {
|
|||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
mainActivity.processGamesDir(result)
|
mainActivity.processGamesDir(result)
|
||||||
gamesDirCallback.onStepCompleted()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
// 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
|
||||||
|
)
|
@ -0,0 +1,83 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
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 _showModInstallPicker = MutableStateFlow(false)
|
||||||
|
val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
|
||||||
|
|
||||||
|
private val _showModNoticeDialog = MutableStateFlow(false)
|
||||||
|
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
|
||||||
|
|
||||||
|
var game: Game? = null
|
||||||
|
|
||||||
|
private val isRefreshing = AtomicBoolean(false)
|
||||||
|
|
||||||
|
fun onOpenAddons(game: Game) {
|
||||||
|
this.game = game
|
||||||
|
refreshAddons()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshAddons() {
|
||||||
|
if (isRefreshing.get() || game == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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
|
||||||
|
isRefreshing.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCloseAddons() {
|
||||||
|
if (_addonList.value.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeConfig.setDisabledAddons(
|
||||||
|
game!!.programId,
|
||||||
|
_addonList.value.mapNotNull {
|
||||||
|
if (it.enabled) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
it.title
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
)
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
_addonList.value.clear()
|
||||||
|
game = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showModInstallPicker(install: Boolean) {
|
||||||
|
_showModInstallPicker.value = install
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showModNoticeDialog(show: Boolean) {
|
||||||
|
_showModNoticeDialog.value = show
|
||||||
|
}
|
||||||
|
}
|
@ -7,60 +7,55 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
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.utils.FileUtil
|
||||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||||
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
|
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DriverViewModel : ViewModel() {
|
class DriverViewModel : ViewModel() {
|
||||||
private val _areDriversLoading = MutableStateFlow(false)
|
private val _areDriversLoading = MutableStateFlow(false)
|
||||||
val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
|
|
||||||
|
|
||||||
private val _isDriverReady = MutableStateFlow(true)
|
private val _isDriverReady = MutableStateFlow(true)
|
||||||
val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
|
|
||||||
|
|
||||||
private val _isDeletingDrivers = MutableStateFlow(false)
|
private val _isDeletingDrivers = MutableStateFlow(false)
|
||||||
val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
|
|
||||||
|
|
||||||
private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>())
|
val isInteractionAllowed: StateFlow<Boolean> =
|
||||||
|
combine(
|
||||||
|
_areDriversLoading,
|
||||||
|
_isDriverReady,
|
||||||
|
_isDeletingDrivers
|
||||||
|
) { loading, ready, deleting ->
|
||||||
|
!loading && ready && !deleting
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
|
||||||
|
|
||||||
|
private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers())
|
||||||
val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
|
val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
|
||||||
|
|
||||||
var previouslySelectedDriver = 0
|
var previouslySelectedDriver = 0
|
||||||
var selectedDriver = -1
|
var selectedDriver = -1
|
||||||
|
|
||||||
private val _selectedDriverMetadata =
|
// Used for showing which driver is currently installed within the driver manager card
|
||||||
MutableStateFlow(
|
private val _selectedDriverTitle = MutableStateFlow("")
|
||||||
GpuDriverHelper.customDriverData.name
|
val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
|
||||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
|
||||||
)
|
|
||||||
val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
|
|
||||||
|
|
||||||
private val _newDriverInstalled = MutableStateFlow(false)
|
private val _newDriverInstalled = MutableStateFlow(false)
|
||||||
val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
|
val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
|
||||||
|
|
||||||
val driversToDelete = mutableListOf<String>()
|
val driversToDelete = mutableListOf<String>()
|
||||||
|
|
||||||
val isInteractionAllowed
|
|
||||||
get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
_areDriversLoading.value = true
|
val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData
|
||||||
viewModelScope.launch {
|
findSelectedDriver(currentDriverMetadata)
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val drivers = GpuDriverHelper.getDrivers()
|
|
||||||
val currentDriverMetadata = GpuDriverHelper.customDriverData
|
|
||||||
for (i in drivers.indices) {
|
|
||||||
if (drivers[i].second == currentDriverMetadata) {
|
|
||||||
setSelectedDriverIndex(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a user had installed a driver before the manager was implemented, this zips
|
// 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
|
// the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
|
||||||
@ -74,14 +69,21 @@ class DriverViewModel : ViewModel() {
|
|||||||
GpuDriverHelper.driverInstallationPath!!,
|
GpuDriverHelper.driverInstallationPath!!,
|
||||||
BufferedOutputStream(driverToSave.outputStream())
|
BufferedOutputStream(driverToSave.outputStream())
|
||||||
)
|
)
|
||||||
drivers.add(Pair(driverToSave.path, currentDriverMetadata))
|
_driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
|
||||||
setSelectedDriverIndex(drivers.size - 1)
|
setSelectedDriverIndex(_driverList.value.size - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
_driverList.value = drivers
|
// If a user had installed a driver before the config was reworked to be multiplatform,
|
||||||
_areDriversLoading.value = false
|
// 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)
|
||||||
}
|
}
|
||||||
|
updateDriverNameForGame(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelectedDriverIndex(value: Int) {
|
fun setSelectedDriverIndex(value: Int) {
|
||||||
@ -98,9 +100,9 @@ class DriverViewModel : ViewModel() {
|
|||||||
fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
|
fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
|
||||||
val driverIndex = _driverList.value.indexOfFirst { it == driverData }
|
val driverIndex = _driverList.value.indexOfFirst { it == driverData }
|
||||||
if (driverIndex == -1) {
|
if (driverIndex == -1) {
|
||||||
setSelectedDriverIndex(_driverList.value.size)
|
|
||||||
_driverList.value.add(driverData)
|
_driverList.value.add(driverData)
|
||||||
_selectedDriverMetadata.value = driverData.second.name
|
setSelectedDriverIndex(_driverList.value.size - 1)
|
||||||
|
_selectedDriverTitle.value = driverData.second.name
|
||||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||||
} else {
|
} else {
|
||||||
setSelectedDriverIndex(driverIndex)
|
setSelectedDriverIndex(driverIndex)
|
||||||
@ -111,8 +113,31 @@ class DriverViewModel : ViewModel() {
|
|||||||
_driverList.value.remove(driverData)
|
_driverList.value.remove(driverData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCloseDriverManager() {
|
fun onOpenDriverManager(game: Game?) {
|
||||||
|
if (game != null) {
|
||||||
|
SettingsFile.loadCustomConfig(game)
|
||||||
|
}
|
||||||
|
|
||||||
|
val driverPath = StringSetting.DRIVER_PATH.getString()
|
||||||
|
if (driverPath.isEmpty()) {
|
||||||
|
setSelectedDriverIndex(0)
|
||||||
|
} else {
|
||||||
|
findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCloseDriverManager(game: Game?) {
|
||||||
_isDeletingDrivers.value = true
|
_isDeletingDrivers.value = true
|
||||||
|
StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
|
||||||
|
updateDriverNameForGame(game)
|
||||||
|
if (game == null) {
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
} else {
|
||||||
|
NativeConfig.savePerGameConfig()
|
||||||
|
NativeConfig.unloadPerGameConfig()
|
||||||
|
NativeConfig.reloadGlobalConfig()
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
driversToDelete.forEach {
|
driversToDelete.forEach {
|
||||||
@ -125,23 +150,29 @@ class DriverViewModel : ViewModel() {
|
|||||||
_isDeletingDrivers.value = false
|
_isDeletingDrivers.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
|
// It is the Emulation Fragment's responsibility to load per-game settings so that this function
|
||||||
|
// knows what driver to load.
|
||||||
|
fun onLaunchGame() {
|
||||||
|
_isDriverReady.value = false
|
||||||
|
|
||||||
|
val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString())
|
||||||
|
val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData
|
||||||
|
if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_isDriverReady.value = false
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (selectedDriver == 0) {
|
if (selectedDriverMetadata.name == null) {
|
||||||
GpuDriverHelper.installDefaultDriver()
|
GpuDriverHelper.installDefaultDriver()
|
||||||
setDriverReady()
|
setDriverReady()
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
|
|
||||||
val driverToInstall = File(driverList.value[selectedDriver].first)
|
if (selectedDriverFile.exists()) {
|
||||||
if (driverToInstall.exists()) {
|
GpuDriverHelper.installCustomDriver(selectedDriverFile)
|
||||||
GpuDriverHelper.installCustomDriver(driverToInstall)
|
|
||||||
} else {
|
} else {
|
||||||
GpuDriverHelper.installDefaultDriver()
|
GpuDriverHelper.installDefaultDriver()
|
||||||
}
|
}
|
||||||
@ -150,9 +181,43 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game == null || NativeConfig.isPerGameConfigLoaded()) {
|
||||||
|
updateName()
|
||||||
|
} else {
|
||||||
|
SettingsFile.loadCustomConfig(game)
|
||||||
|
updateName()
|
||||||
|
NativeConfig.unloadPerGameConfig()
|
||||||
|
NativeConfig.reloadGlobalConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateName() {
|
||||||
|
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
|
||||||
|
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setDriverReady() {
|
private fun setDriverReady() {
|
||||||
_isDriverReady.value = true
|
_isDriverReady.value = true
|
||||||
_selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
|
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
|
||||||
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,18 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import java.util.HashSet
|
import java.util.HashSet
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
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.utils.DirectoryInitialization
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -15,12 +23,44 @@ class Game(
|
|||||||
val path: String,
|
val path: String,
|
||||||
val programId: String = "",
|
val programId: String = "",
|
||||||
val developer: String = "",
|
val developer: String = "",
|
||||||
val version: String = "",
|
var version: String = "",
|
||||||
val isHomebrew: Boolean = false
|
val isHomebrew: Boolean = false
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
|
val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
|
||||||
val keyLastPlayedTime get() = "${path}_LastPlayed"
|
val keyLastPlayedTime get() = "${path}_LastPlayed"
|
||||||
|
|
||||||
|
val settingsName: String
|
||||||
|
get() {
|
||||||
|
val programIdLong = programId.toLong()
|
||||||
|
return if (programIdLong == 0L) {
|
||||||
|
FileUtil.getFilename(Uri.parse(path))
|
||||||
|
} else {
|
||||||
|
"0" + programIdLong.toString(16).uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val programIdHex: String
|
||||||
|
get() {
|
||||||
|
val programIdLong = programId.toLong()
|
||||||
|
return if (programIdLong == 0L) {
|
||||||
|
"0"
|
||||||
|
} else {
|
||||||
|
"0" + programIdLong.toString(16).uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveZipName: String
|
||||||
|
get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${
|
||||||
|
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
||||||
|
}.zip"
|
||||||
|
|
||||||
|
val saveDir: String
|
||||||
|
get() = DirectoryInitialization.userDirectory + "/nand" +
|
||||||
|
NativeLibrary.getSavePath(programId)
|
||||||
|
|
||||||
|
val addonDir: String
|
||||||
|
get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other !is Game) {
|
if (other !is Game) {
|
||||||
return false
|
return false
|
||||||
@ -34,6 +74,7 @@ class Game(
|
|||||||
result = 31 * result + path.hashCode()
|
result = 31 * result + path.hashCode()
|
||||||
result = 31 * result + programId.hashCode()
|
result = 31 * result + programId.hashCode()
|
||||||
result = 31 * result + developer.hashCode()
|
result = 31 * result + developer.hashCode()
|
||||||
|
result = 31 * result + version.hashCode()
|
||||||
result = 31 * result + isHomebrew.hashCode()
|
result = 31 * result + isHomebrew.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface GameProperty {
|
||||||
|
@get:StringRes
|
||||||
|
val titleId: Int
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val descriptionId: Int
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val iconId: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SubmenuProperty(
|
||||||
|
override val titleId: Int,
|
||||||
|
override val descriptionId: Int,
|
||||||
|
override val iconId: Int,
|
||||||
|
val details: (() -> String)? = null,
|
||||||
|
val detailsFlow: StateFlow<String>? = null,
|
||||||
|
val action: () -> Unit
|
||||||
|
) : GameProperty
|
||||||
|
|
||||||
|
data class InstallableProperty(
|
||||||
|
override val titleId: Int,
|
||||||
|
override val descriptionId: Int,
|
||||||
|
override val iconId: Int,
|
||||||
|
val install: (() -> Unit)? = null,
|
||||||
|
val export: (() -> Unit)? = null
|
||||||
|
) : GameProperty
|
@ -20,8 +20,8 @@ import kotlinx.serialization.json.Json
|
|||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||||
import org.yuzu.yuzu_emu.utils.GameMetadata
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class GamesViewModel : ViewModel() {
|
class GamesViewModel : ViewModel() {
|
||||||
val games: StateFlow<List<Game>> get() = _games
|
val games: StateFlow<List<Game>> get() = _games
|
||||||
@ -33,6 +33,8 @@ class GamesViewModel : ViewModel() {
|
|||||||
val isReloading: StateFlow<Boolean> get() = _isReloading
|
val isReloading: StateFlow<Boolean> get() = _isReloading
|
||||||
private val _isReloading = MutableStateFlow(false)
|
private val _isReloading = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private val reloading = AtomicBoolean(false)
|
||||||
|
|
||||||
val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
|
val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
|
||||||
private val _shouldSwapData = MutableStateFlow(false)
|
private val _shouldSwapData = MutableStateFlow(false)
|
||||||
|
|
||||||
@ -49,38 +51,8 @@ class GamesViewModel : ViewModel() {
|
|||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
|
||||||
// Retrieve list of cached games
|
|
||||||
val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
.getStringSet(GameHelper.KEY_GAMES, emptySet())
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
getGameDirs()
|
getGameDirs()
|
||||||
if (storedGames!!.isNotEmpty()) {
|
reloadGames(directoriesChanged = false, firstStartup = true)
|
||||||
val deserializedGames = mutableSetOf<Game>()
|
|
||||||
storedGames.forEach {
|
|
||||||
val game: Game
|
|
||||||
try {
|
|
||||||
game = Json.decodeFromString(it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// We don't care about any errors related to parsing the game cache
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val gameExists =
|
|
||||||
DocumentFile.fromSingleUri(
|
|
||||||
YuzuApplication.appContext,
|
|
||||||
Uri.parse(game.path)
|
|
||||||
)?.exists()
|
|
||||||
if (gameExists == true) {
|
|
||||||
deserializedGames.add(game)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setGames(deserializedGames.toList())
|
|
||||||
}
|
|
||||||
reloadGames(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGames(games: List<Game>) {
|
fun setGames(games: List<Game>) {
|
||||||
@ -110,16 +82,46 @@ class GamesViewModel : ViewModel() {
|
|||||||
_searchFocused.value = searchFocused
|
_searchFocused.value = searchFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadGames(directoriesChanged: Boolean) {
|
fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
|
||||||
if (isReloading.value) {
|
if (reloading.get()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
reloading.set(true)
|
||||||
_isReloading.value = true
|
_isReloading.value = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
GameMetadata.resetMetadata()
|
if (firstStartup) {
|
||||||
|
// Retrieve list of cached games
|
||||||
|
val storedGames =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
.getStringSet(GameHelper.KEY_GAMES, emptySet())
|
||||||
|
if (storedGames!!.isNotEmpty()) {
|
||||||
|
val deserializedGames = mutableSetOf<Game>()
|
||||||
|
storedGames.forEach {
|
||||||
|
val game: Game
|
||||||
|
try {
|
||||||
|
game = Json.decodeFromString(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// We don't care about any errors related to parsing the game cache
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val gameExists =
|
||||||
|
DocumentFile.fromSingleUri(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
Uri.parse(game.path)
|
||||||
|
)?.exists()
|
||||||
|
if (gameExists == true) {
|
||||||
|
deserializedGames.add(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setGames(deserializedGames.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setGames(GameHelper.getGames())
|
setGames(GameHelper.getGames())
|
||||||
|
reloading.set(false)
|
||||||
_isReloading.value = false
|
_isReloading.value = false
|
||||||
|
|
||||||
if (directoriesChanged) {
|
if (directoriesChanged) {
|
||||||
@ -133,7 +135,7 @@ class GamesViewModel : ViewModel() {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
NativeConfig.addGameDir(gameDir)
|
NativeConfig.addGameDir(gameDir)
|
||||||
getGameDirs()
|
getGameDirs(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +170,7 @@ class GamesViewModel : ViewModel() {
|
|||||||
fun onCloseGameFoldersFragment() =
|
fun onCloseGameFoldersFragment() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
getGameDirs(true)
|
getGameDirs(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
||||||
@ -17,6 +19,18 @@ class HomeViewModel : ViewModel() {
|
|||||||
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
||||||
private val _shouldPageForward = MutableStateFlow(false)
|
private val _shouldPageForward = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private val _gamesDirSelected = MutableStateFlow(false)
|
||||||
|
val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
|
||||||
|
|
||||||
|
private val _openImportSaves = MutableStateFlow(false)
|
||||||
|
val openImportSaves get() = _openImportSaves.asStateFlow()
|
||||||
|
|
||||||
|
private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
|
||||||
|
val contentToInstall get() = _contentToInstall.asStateFlow()
|
||||||
|
|
||||||
|
private val _reloadPropertiesList = MutableStateFlow(false)
|
||||||
|
val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
|
||||||
|
|
||||||
var navigatedToSetup = false
|
var navigatedToSetup = false
|
||||||
|
|
||||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||||
@ -36,4 +50,20 @@ class HomeViewModel : ViewModel() {
|
|||||||
fun setShouldPageForward(pageForward: Boolean) {
|
fun setShouldPageForward(pageForward: Boolean) {
|
||||||
_shouldPageForward.value = pageForward
|
_shouldPageForward.value = pageForward
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setGamesDirSelected(selected: Boolean) {
|
||||||
|
_gamesDirSelected.value = selected
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOpenImportSaves(import: Boolean) {
|
||||||
|
_openImportSaves.value = import
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContentToInstall(documents: List<Uri>?) {
|
||||||
|
_contentToInstall.value = documents
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reloadPropertiesList(reload: Boolean) {
|
||||||
|
_reloadPropertiesList.value = reload
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class MessageDialogViewModel : ViewModel() {
|
class MessageDialogViewModel : ViewModel() {
|
||||||
var dismissAction: () -> Unit = {}
|
var positiveAction: (() -> Unit)? = null
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
dismissAction = {}
|
positiveAction = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() {
|
|||||||
fun setAdapterItemChanged(value: Int) {
|
fun setAdapterItemChanged(value: Int) {
|
||||||
_adapterItemChanged.value = value
|
_adapterItemChanged.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
game = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() {
|
|||||||
val cancelled: StateFlow<Boolean> get() = _cancelled
|
val cancelled: StateFlow<Boolean> get() = _cancelled
|
||||||
private val _cancelled = MutableStateFlow(false)
|
private val _cancelled = MutableStateFlow(false)
|
||||||
|
|
||||||
lateinit var task: () -> Any
|
lateinit var task: suspend () -> Any
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
_result.value = Any()
|
_result.value = Any()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ import android.graphics.Rect
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
|
import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
|
||||||
|
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom [BitmapDrawable] that is capable
|
* Custom [BitmapDrawable] that is capable
|
||||||
@ -25,7 +26,7 @@ class InputOverlayDrawableButton(
|
|||||||
defaultStateBitmap: Bitmap,
|
defaultStateBitmap: Bitmap,
|
||||||
pressedStateBitmap: Bitmap,
|
pressedStateBitmap: Bitmap,
|
||||||
val buttonId: Int,
|
val buttonId: Int,
|
||||||
val prefId: String
|
val overlayControlData: OverlayControlData
|
||||||
) {
|
) {
|
||||||
// The ID value what motion event is tracking
|
// The ID value what motion event is tracking
|
||||||
var trackId: Int
|
var trackId: Int
|
||||||
|
@ -14,7 +14,7 @@ import kotlin.math.cos
|
|||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
|
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom [BitmapDrawable] that is capable
|
* Custom [BitmapDrawable] that is capable
|
||||||
@ -125,7 +125,7 @@ class InputOverlayDrawableJoystick(
|
|||||||
pressedState = true
|
pressedState = true
|
||||||
outerBitmap.alpha = 0
|
outerBitmap.alpha = 0
|
||||||
boundsBoxBitmap.alpha = opacity
|
boundsBoxBitmap.alpha = opacity
|
||||||
if (EmulationMenuSettings.joystickRelCenter) {
|
if (BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()) {
|
||||||
virtBounds.offset(
|
virtBounds.offset(
|
||||||
xPosition - virtBounds.centerX(),
|
xPosition - virtBounds.centerX(),
|
||||||
yPosition - virtBounds.centerY()
|
yPosition - virtBounds.centerY()
|
||||||
|
@ -0,0 +1,188 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.overlay.model
|
||||||
|
|
||||||
|
import androidx.annotation.IntegerRes
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
|
||||||
|
enum class OverlayControl(
|
||||||
|
val id: String,
|
||||||
|
val defaultVisibility: Boolean,
|
||||||
|
@IntegerRes val defaultLandscapePositionResources: Pair<Int, Int>,
|
||||||
|
@IntegerRes val defaultPortraitPositionResources: Pair<Int, Int>,
|
||||||
|
@IntegerRes val defaultFoldablePositionResources: Pair<Int, Int>
|
||||||
|
) {
|
||||||
|
BUTTON_A(
|
||||||
|
"button_a",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_A_X, R.integer.BUTTON_A_Y),
|
||||||
|
Pair(R.integer.BUTTON_A_X_PORTRAIT, R.integer.BUTTON_A_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_B(
|
||||||
|
"button_b",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_B_X, R.integer.BUTTON_B_Y),
|
||||||
|
Pair(R.integer.BUTTON_B_X_PORTRAIT, R.integer.BUTTON_B_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_X(
|
||||||
|
"button_x",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_X_X, R.integer.BUTTON_X_Y),
|
||||||
|
Pair(R.integer.BUTTON_X_X_PORTRAIT, R.integer.BUTTON_X_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_Y(
|
||||||
|
"button_y",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_Y_X, R.integer.BUTTON_Y_Y),
|
||||||
|
Pair(R.integer.BUTTON_Y_X_PORTRAIT, R.integer.BUTTON_Y_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_PLUS(
|
||||||
|
"button_plus",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_PLUS_X, R.integer.BUTTON_PLUS_Y),
|
||||||
|
Pair(R.integer.BUTTON_PLUS_X_PORTRAIT, R.integer.BUTTON_PLUS_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_MINUS(
|
||||||
|
"button_minus",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_MINUS_X, R.integer.BUTTON_MINUS_Y),
|
||||||
|
Pair(R.integer.BUTTON_MINUS_X_PORTRAIT, R.integer.BUTTON_MINUS_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_HOME(
|
||||||
|
"button_home",
|
||||||
|
false,
|
||||||
|
Pair(R.integer.BUTTON_HOME_X, R.integer.BUTTON_HOME_Y),
|
||||||
|
Pair(R.integer.BUTTON_HOME_X_PORTRAIT, R.integer.BUTTON_HOME_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_CAPTURE(
|
||||||
|
"button_capture",
|
||||||
|
false,
|
||||||
|
Pair(R.integer.BUTTON_CAPTURE_X, R.integer.BUTTON_CAPTURE_Y),
|
||||||
|
Pair(R.integer.BUTTON_CAPTURE_X_PORTRAIT, R.integer.BUTTON_CAPTURE_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_L(
|
||||||
|
"button_l",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_L_X, R.integer.BUTTON_L_Y),
|
||||||
|
Pair(R.integer.BUTTON_L_X_PORTRAIT, R.integer.BUTTON_L_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_R(
|
||||||
|
"button_r",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_R_X, R.integer.BUTTON_R_Y),
|
||||||
|
Pair(R.integer.BUTTON_R_X_PORTRAIT, R.integer.BUTTON_R_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_ZL(
|
||||||
|
"button_zl",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_ZL_X, R.integer.BUTTON_ZL_Y),
|
||||||
|
Pair(R.integer.BUTTON_ZL_X_PORTRAIT, R.integer.BUTTON_ZL_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_ZR(
|
||||||
|
"button_zr",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_ZR_X, R.integer.BUTTON_ZR_Y),
|
||||||
|
Pair(R.integer.BUTTON_ZR_X_PORTRAIT, R.integer.BUTTON_ZR_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_STICK_L(
|
||||||
|
"button_stick_l",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_STICK_L_X, R.integer.BUTTON_STICK_L_Y),
|
||||||
|
Pair(R.integer.BUTTON_STICK_L_X_PORTRAIT, R.integer.BUTTON_STICK_L_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
BUTTON_STICK_R(
|
||||||
|
"button_stick_r",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.BUTTON_STICK_R_X, R.integer.BUTTON_STICK_R_Y),
|
||||||
|
Pair(R.integer.BUTTON_STICK_R_X_PORTRAIT, R.integer.BUTTON_STICK_R_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
STICK_L(
|
||||||
|
"stick_l",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.STICK_L_X, R.integer.STICK_L_Y),
|
||||||
|
Pair(R.integer.STICK_L_X_PORTRAIT, R.integer.STICK_L_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
STICK_R(
|
||||||
|
"stick_r",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.STICK_R_X, R.integer.STICK_R_Y),
|
||||||
|
Pair(R.integer.STICK_R_X_PORTRAIT, R.integer.STICK_R_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE)
|
||||||
|
),
|
||||||
|
COMBINED_DPAD(
|
||||||
|
"combined_dpad",
|
||||||
|
true,
|
||||||
|
Pair(R.integer.COMBINED_DPAD_X, R.integer.COMBINED_DPAD_Y),
|
||||||
|
Pair(R.integer.COMBINED_DPAD_X_PORTRAIT, R.integer.COMBINED_DPAD_Y_PORTRAIT),
|
||||||
|
Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE)
|
||||||
|
);
|
||||||
|
|
||||||
|
fun getDefaultPositionForLayout(layout: OverlayLayout): Pair<Double, Double> {
|
||||||
|
val rawResourcePair: Pair<Int, Int>
|
||||||
|
YuzuApplication.appContext.resources.apply {
|
||||||
|
rawResourcePair = when (layout) {
|
||||||
|
OverlayLayout.Landscape -> {
|
||||||
|
Pair(
|
||||||
|
getInteger(this@OverlayControl.defaultLandscapePositionResources.first),
|
||||||
|
getInteger(this@OverlayControl.defaultLandscapePositionResources.second)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayLayout.Portrait -> {
|
||||||
|
Pair(
|
||||||
|
getInteger(this@OverlayControl.defaultPortraitPositionResources.first),
|
||||||
|
getInteger(this@OverlayControl.defaultPortraitPositionResources.second)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayLayout.Foldable -> {
|
||||||
|
Pair(
|
||||||
|
getInteger(this@OverlayControl.defaultFoldablePositionResources.first),
|
||||||
|
getInteger(this@OverlayControl.defaultFoldablePositionResources.second)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(
|
||||||
|
rawResourcePair.first.toDouble() / 1000,
|
||||||
|
rawResourcePair.second.toDouble() / 1000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toOverlayControlData(): OverlayControlData =
|
||||||
|
OverlayControlData(
|
||||||
|
id,
|
||||||
|
defaultVisibility,
|
||||||
|
getDefaultPositionForLayout(OverlayLayout.Landscape),
|
||||||
|
getDefaultPositionForLayout(OverlayLayout.Portrait),
|
||||||
|
getDefaultPositionForLayout(OverlayLayout.Foldable)
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val map: HashMap<String, OverlayControl> by lazy {
|
||||||
|
val hashMap = hashMapOf<String, OverlayControl>()
|
||||||
|
entries.forEach { hashMap[it.id] = it }
|
||||||
|
hashMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(id: String): OverlayControl? = map[id]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.overlay.model
|
||||||
|
|
||||||
|
data class OverlayControlData(
|
||||||
|
val id: String,
|
||||||
|
var enabled: Boolean,
|
||||||
|
var landscapePosition: Pair<Double, Double>,
|
||||||
|
var portraitPosition: Pair<Double, Double>,
|
||||||
|
var foldablePosition: Pair<Double, Double>
|
||||||
|
) {
|
||||||
|
fun positionFromLayout(layout: OverlayLayout): Pair<Double, Double> =
|
||||||
|
when (layout) {
|
||||||
|
OverlayLayout.Landscape -> landscapePosition
|
||||||
|
OverlayLayout.Portrait -> portraitPosition
|
||||||
|
OverlayLayout.Foldable -> foldablePosition
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.overlay.model
|
||||||
|
|
||||||
|
import androidx.annotation.IntegerRes
|
||||||
|
|
||||||
|
data class OverlayControlDefault(
|
||||||
|
val buttonId: String,
|
||||||
|
@IntegerRes val landscapePositionResource: Pair<Int, Int>,
|
||||||
|
@IntegerRes val portraitPositionResource: Pair<Int, Int>,
|
||||||
|
@IntegerRes val foldablePositionResource: Pair<Int, Int>
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.overlay.model
|
||||||
|
|
||||||
|
enum class OverlayLayout(val id: String) {
|
||||||
|
Landscape("Landscape"),
|
||||||
|
Portrait("Portrait"),
|
||||||
|
Foldable("Foldable")
|
||||||
|
}
|
@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.transition.MaterialFadeThrough
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||||
@ -35,11 +35,6 @@ class GamesFragment : Fragment() {
|
|||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enterTransition = MaterialFadeThrough()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@ -52,7 +47,9 @@ class GamesFragment : Fragment() {
|
|||||||
// This is using the correct scope, lint is just acting up
|
// This is using the correct scope, lint is just acting up
|
||||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
homeViewModel.setNavigationVisibility(visible = true, animated = false)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(true)
|
||||||
|
|
||||||
binding.gridGames.apply {
|
binding.gridGames.apply {
|
||||||
layoutManager = AutofitGridLayoutManager(
|
layoutManager = AutofitGridLayoutManager(
|
||||||
@ -94,18 +91,20 @@ class GamesFragment : Fragment() {
|
|||||||
viewLifecycleOwner.lifecycleScope.apply {
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
launch {
|
launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
gamesViewModel.isReloading.collect { binding.swipeRefresh.isRefreshing = it }
|
gamesViewModel.isReloading.collect {
|
||||||
|
binding.swipeRefresh.isRefreshing = it
|
||||||
|
if (gamesViewModel.games.value.isEmpty() && !it) {
|
||||||
|
binding.noticeText.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.noticeText.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
gamesViewModel.games.collect {
|
gamesViewModel.games.collectLatest {
|
||||||
(binding.gridGames.adapter as GameAdapter).submitList(it)
|
(binding.gridGames.adapter as GameAdapter).submitList(it)
|
||||||
if (it.isEmpty()) {
|
|
||||||
binding.noticeText.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.noticeText.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FilenameFilter
|
import java.io.FilenameFilter
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
@ -43,7 +40,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
|
|||||||
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
import org.yuzu.yuzu_emu.getPublicFilesDir
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
import org.yuzu.yuzu_emu.model.TaskState
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
@ -60,15 +57,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
private val homeViewModel: HomeViewModel by viewModels()
|
private val homeViewModel: HomeViewModel by viewModels()
|
||||||
private val gamesViewModel: GamesViewModel by viewModels()
|
private val gamesViewModel: GamesViewModel by viewModels()
|
||||||
private val taskViewModel: TaskViewModel by viewModels()
|
private val taskViewModel: TaskViewModel by viewModels()
|
||||||
|
private val addonViewModel: AddonViewModel by viewModels()
|
||||||
|
|
||||||
override var themeId: Int = 0
|
override var themeId: Int = 0
|
||||||
|
|
||||||
private val savesFolder
|
|
||||||
get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
|
|
||||||
|
|
||||||
// Get first subfolder in saves folder (should be the user folder)
|
|
||||||
val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
||||||
@ -145,6 +137,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
|
homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
homeViewModel.contentToInstall.collect {
|
||||||
|
if (it != null) {
|
||||||
|
installContent(it)
|
||||||
|
homeViewModel.setContentToInstall(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss previous notifications (should not happen unless a crash occurred)
|
// Dismiss previous notifications (should not happen unless a crash occurred)
|
||||||
@ -253,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
NativeConfig.saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
EmulationActivity.stopForegroundService(this)
|
EmulationActivity.stopForegroundService(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
@ -468,7 +463,46 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
val installGameUpdate = registerForActivityResult(
|
val installGameUpdate = registerForActivityResult(
|
||||||
ActivityResultContracts.OpenMultipleDocuments()
|
ActivityResultContracts.OpenMultipleDocuments()
|
||||||
) { documents: List<Uri> ->
|
) { documents: List<Uri> ->
|
||||||
if (documents.isNotEmpty()) {
|
if (documents.isEmpty()) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addonViewModel.game == null) {
|
||||||
|
installContent(documents)
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
this@MainActivity,
|
||||||
|
R.string.verifying_content,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
var updatesMatchProgram = true
|
||||||
|
for (document in documents) {
|
||||||
|
val valid = NativeLibrary.doesUpdateMatchProgram(
|
||||||
|
addonViewModel.game!!.programId,
|
||||||
|
document.toString()
|
||||||
|
)
|
||||||
|
if (!valid) {
|
||||||
|
updatesMatchProgram = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatesMatchProgram) {
|
||||||
|
homeViewModel.setContentToInstall(documents)
|
||||||
|
} else {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
this@MainActivity,
|
||||||
|
titleId = R.string.content_install_notice,
|
||||||
|
descriptionId = R.string.content_install_notice_description,
|
||||||
|
positiveAction = { homeViewModel.setContentToInstall(documents) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installContent(documents: List<Uri>) {
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
R.string.installing_game_content
|
R.string.installing_game_content
|
||||||
@ -507,6 +541,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addonViewModel.refreshAddons()
|
||||||
|
|
||||||
val separator = System.getProperty("line.separator") ?: "\n"
|
val separator = System.getProperty("line.separator") ?: "\n"
|
||||||
val installResult = StringBuilder()
|
val installResult = StringBuilder()
|
||||||
if (installSuccess > 0) {
|
if (installSuccess > 0) {
|
||||||
@ -572,7 +608,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
}
|
}
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val exportUserData = registerForActivityResult(
|
val exportUserData = registerForActivityResult(
|
||||||
ActivityResultContracts.CreateDocument("application/zip")
|
ActivityResultContracts.CreateDocument("application/zip")
|
||||||
@ -632,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing user data
|
// Clear existing user data
|
||||||
NativeConfig.unloadConfig()
|
NativeConfig.unloadGlobalConfig()
|
||||||
File(DirectoryInitialization.userDirectory!!).deleteRecursively()
|
File(DirectoryInitialization.userDirectory!!).deleteRecursively()
|
||||||
|
|
||||||
// Copy archive to internal storage
|
// Copy archive to internal storage
|
||||||
@ -651,108 +686,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
|
|
||||||
// Reinitialize relevant data
|
// Reinitialize relevant data
|
||||||
NativeLibrary.initializeSystem(true)
|
NativeLibrary.initializeSystem(true)
|
||||||
NativeConfig.initializeConfig()
|
NativeConfig.initializeGlobalConfig()
|
||||||
gamesViewModel.reloadGames(false)
|
gamesViewModel.reloadGames(false)
|
||||||
|
|
||||||
return@newInstance getString(R.string.user_data_import_success)
|
return@newInstance getString(R.string.user_data_import_success)
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
|
|
||||||
*/
|
|
||||||
val exportSaves = registerForActivityResult(
|
|
||||||
ActivityResultContracts.CreateDocument("application/zip")
|
|
||||||
) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
R.string.save_files_exporting,
|
|
||||||
false
|
|
||||||
) {
|
|
||||||
val zipResult = FileUtil.zipFromInternalStorage(
|
|
||||||
File(savesFolderRoot),
|
|
||||||
savesFolderRoot,
|
|
||||||
BufferedOutputStream(contentResolver.openOutputStream(result))
|
|
||||||
)
|
|
||||||
return@newInstance when (zipResult) {
|
|
||||||
TaskState.Completed -> getString(R.string.export_success)
|
|
||||||
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
|
||||||
}
|
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val startForResultExportSave =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
|
|
||||||
File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
val importSaves =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.initializeEmptyUserDirectory()
|
|
||||||
|
|
||||||
val inputZip = contentResolver.openInputStream(result)
|
|
||||||
// A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
|
|
||||||
var validZip = false
|
|
||||||
val savesFolder = File(savesFolderRoot)
|
|
||||||
val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
|
|
||||||
cacheSaveDir.mkdir()
|
|
||||||
|
|
||||||
if (inputZip == null) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.fatal_error),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val filterTitleId =
|
|
||||||
FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
|
||||||
cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
|
|
||||||
File(savesFolder, savePath).deleteRecursively()
|
|
||||||
File(cacheSaveDir, savePath).copyRecursively(
|
|
||||||
File(savesFolder, savePath),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
validZip = true
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
if (!validZip) {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this@MainActivity,
|
|
||||||
titleId = R.string.save_file_invalid_zip_structure,
|
|
||||||
descriptionId = R.string.save_file_invalid_zip_structure_description
|
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.save_file_imported_success),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheSaveDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.fatal_error),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
object AddonUtil {
|
||||||
|
val validAddonDirectories = listOf("cheats", "exefs", "romfs")
|
||||||
|
}
|
@ -3,9 +3,17 @@
|
|||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
|
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
|
||||||
|
import org.yuzu.yuzu_emu.overlay.model.OverlayControl
|
||||||
|
import org.yuzu.yuzu_emu.overlay.model.OverlayLayout
|
||||||
|
import org.yuzu.yuzu_emu.utils.PreferenceUtil.migratePreference
|
||||||
|
|
||||||
object DirectoryInitialization {
|
object DirectoryInitialization {
|
||||||
private var userPath: String? = null
|
private var userPath: String? = null
|
||||||
@ -16,7 +24,8 @@ object DirectoryInitialization {
|
|||||||
if (!areDirectoriesReady) {
|
if (!areDirectoriesReady) {
|
||||||
initializeInternalStorage()
|
initializeInternalStorage()
|
||||||
NativeLibrary.initializeSystem(false)
|
NativeLibrary.initializeSystem(false)
|
||||||
NativeConfig.initializeConfig()
|
NativeConfig.initializeGlobalConfig()
|
||||||
|
migrateSettings()
|
||||||
areDirectoriesReady = true
|
areDirectoriesReady = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,4 +44,170 @@ object DirectoryInitialization {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun migrateSettings() {
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
var saveConfig = false
|
||||||
|
val theme = preferences.migratePreference<Int>(Settings.PREF_THEME)
|
||||||
|
if (theme != null) {
|
||||||
|
IntSetting.THEME.setInt(theme)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val themeMode = preferences.migratePreference<Int>(Settings.PREF_THEME_MODE)
|
||||||
|
if (themeMode != null) {
|
||||||
|
IntSetting.THEME_MODE.setInt(themeMode)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val blackBackgrounds =
|
||||||
|
preferences.migratePreference<Boolean>(Settings.PREF_BLACK_BACKGROUNDS)
|
||||||
|
if (blackBackgrounds != null) {
|
||||||
|
BooleanSetting.BLACK_BACKGROUNDS.setBoolean(blackBackgrounds)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val joystickRelCenter =
|
||||||
|
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER)
|
||||||
|
if (joystickRelCenter != null) {
|
||||||
|
BooleanSetting.JOYSTICK_REL_CENTER.setBoolean(joystickRelCenter)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val dpadSlide =
|
||||||
|
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE)
|
||||||
|
if (dpadSlide != null) {
|
||||||
|
BooleanSetting.DPAD_SLIDE.setBoolean(dpadSlide)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val hapticFeedback =
|
||||||
|
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_HAPTICS)
|
||||||
|
if (hapticFeedback != null) {
|
||||||
|
BooleanSetting.HAPTIC_FEEDBACK.setBoolean(hapticFeedback)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val showPerformanceOverlay =
|
||||||
|
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_SHOW_FPS)
|
||||||
|
if (showPerformanceOverlay != null) {
|
||||||
|
BooleanSetting.SHOW_PERFORMANCE_OVERLAY.setBoolean(showPerformanceOverlay)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val showInputOverlay =
|
||||||
|
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY)
|
||||||
|
if (showInputOverlay != null) {
|
||||||
|
BooleanSetting.SHOW_INPUT_OVERLAY.setBoolean(showInputOverlay)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val overlayOpacity = preferences.migratePreference<Int>(Settings.PREF_CONTROL_OPACITY)
|
||||||
|
if (overlayOpacity != null) {
|
||||||
|
IntSetting.OVERLAY_OPACITY.setInt(overlayOpacity)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val overlayScale = preferences.migratePreference<Int>(Settings.PREF_CONTROL_SCALE)
|
||||||
|
if (overlayScale != null) {
|
||||||
|
IntSetting.OVERLAY_SCALE.setInt(overlayScale)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var setOverlayData = false
|
||||||
|
val overlayControlData = NativeConfig.getOverlayControlData()
|
||||||
|
if (overlayControlData.isEmpty()) {
|
||||||
|
val overlayControlDataMap =
|
||||||
|
NativeConfig.getOverlayControlData().associateBy { it.id }.toMutableMap()
|
||||||
|
for (button in Settings.overlayPreferences) {
|
||||||
|
val buttonId = convertButtonId(button)
|
||||||
|
var buttonEnabled = preferences.migratePreference<Boolean>(button)
|
||||||
|
if (buttonEnabled == null) {
|
||||||
|
buttonEnabled = OverlayControl.map[buttonId]?.defaultVisibility == true
|
||||||
|
}
|
||||||
|
|
||||||
|
var landscapeXPosition = preferences.migratePreference<Float>(
|
||||||
|
"$button-X${Settings.PREF_LANDSCAPE_SUFFIX}"
|
||||||
|
)?.toDouble()
|
||||||
|
var landscapeYPosition = preferences.migratePreference<Float>(
|
||||||
|
"$button-Y${Settings.PREF_LANDSCAPE_SUFFIX}"
|
||||||
|
)?.toDouble()
|
||||||
|
if (landscapeXPosition == null || landscapeYPosition == null) {
|
||||||
|
val landscapePosition = OverlayControl.map[buttonId]
|
||||||
|
?.getDefaultPositionForLayout(OverlayLayout.Landscape) ?: Pair(0.0, 0.0)
|
||||||
|
landscapeXPosition = landscapePosition.first
|
||||||
|
landscapeYPosition = landscapePosition.second
|
||||||
|
}
|
||||||
|
|
||||||
|
var portraitXPosition = preferences.migratePreference<Float>(
|
||||||
|
"$button-X${Settings.PREF_PORTRAIT_SUFFIX}"
|
||||||
|
)?.toDouble()
|
||||||
|
var portraitYPosition = preferences.migratePreference<Float>(
|
||||||
|
"$button-Y${Settings.PREF_PORTRAIT_SUFFIX}"
|
||||||
|
)?.toDouble()
|
||||||
|
if (portraitXPosition == null || portraitYPosition == null) {
|
||||||
|
val portraitPosition = OverlayControl.map[buttonId]
|
||||||
|
?.getDefaultPositionForLayout(OverlayLayout.Portrait) ?: Pair(0.0, 0.0)
|
||||||
|
portraitXPosition = portraitPosition.first
|
||||||
|
portraitYPosition = portraitPosition.second
|
||||||
|
}
|
||||||
|
|
||||||
|
var foldableXPosition = preferences.migratePreference<Float>(
|
||||||
|
"$button-X${Settings.PREF_FOLDABLE_SUFFIX}"
|
||||||
|
)?.toDouble()
|
||||||
|
var foldableYPosition = preferences.migratePreference<Float>(
|
||||||
|
"$button-Y${Settings.PREF_FOLDABLE_SUFFIX}"
|
||||||
|
)?.toDouble()
|
||||||
|
if (foldableXPosition == null || foldableYPosition == null) {
|
||||||
|
val foldablePosition = OverlayControl.map[buttonId]
|
||||||
|
?.getDefaultPositionForLayout(OverlayLayout.Foldable) ?: Pair(0.0, 0.0)
|
||||||
|
foldableXPosition = foldablePosition.first
|
||||||
|
foldableYPosition = foldablePosition.second
|
||||||
|
}
|
||||||
|
|
||||||
|
val controlData = OverlayControlData(
|
||||||
|
buttonId,
|
||||||
|
buttonEnabled,
|
||||||
|
Pair(landscapeXPosition, landscapeYPosition),
|
||||||
|
Pair(portraitXPosition, portraitYPosition),
|
||||||
|
Pair(foldableXPosition, foldableYPosition)
|
||||||
|
)
|
||||||
|
overlayControlDataMap[buttonId] = controlData
|
||||||
|
setOverlayData = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setOverlayData) {
|
||||||
|
NativeConfig.setOverlayControlData(
|
||||||
|
overlayControlDataMap.map { it.value }.toTypedArray()
|
||||||
|
)
|
||||||
|
saveConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveConfig) {
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertButtonId(buttonId: String): String =
|
||||||
|
when (buttonId) {
|
||||||
|
Settings.PREF_BUTTON_A -> OverlayControl.BUTTON_A.id
|
||||||
|
Settings.PREF_BUTTON_B -> OverlayControl.BUTTON_B.id
|
||||||
|
Settings.PREF_BUTTON_X -> OverlayControl.BUTTON_X.id
|
||||||
|
Settings.PREF_BUTTON_Y -> OverlayControl.BUTTON_Y.id
|
||||||
|
Settings.PREF_BUTTON_L -> OverlayControl.BUTTON_L.id
|
||||||
|
Settings.PREF_BUTTON_R -> OverlayControl.BUTTON_R.id
|
||||||
|
Settings.PREF_BUTTON_ZL -> OverlayControl.BUTTON_ZL.id
|
||||||
|
Settings.PREF_BUTTON_ZR -> OverlayControl.BUTTON_ZR.id
|
||||||
|
Settings.PREF_BUTTON_PLUS -> OverlayControl.BUTTON_PLUS.id
|
||||||
|
Settings.PREF_BUTTON_MINUS -> OverlayControl.BUTTON_MINUS.id
|
||||||
|
Settings.PREF_BUTTON_DPAD -> OverlayControl.COMBINED_DPAD.id
|
||||||
|
Settings.PREF_STICK_L -> OverlayControl.STICK_L.id
|
||||||
|
Settings.PREF_STICK_R -> OverlayControl.STICK_R.id
|
||||||
|
Settings.PREF_BUTTON_HOME -> OverlayControl.BUTTON_HOME.id
|
||||||
|
Settings.PREF_BUTTON_SCREENSHOT -> OverlayControl.BUTTON_CAPTURE.id
|
||||||
|
Settings.PREF_BUTTON_STICK_L -> OverlayControl.BUTTON_STICK_L.id
|
||||||
|
Settings.PREF_BUTTON_STICK_R -> OverlayControl.BUTTON_STICK_R.id
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
|
||||||
|
|
||||||
object EmulationMenuSettings {
|
|
||||||
private val preferences =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
|
|
||||||
var joystickRelCenter: Boolean
|
|
||||||
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
|
|
||||||
set(value) {
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, value)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
var dpadSlide: Boolean
|
|
||||||
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, true)
|
|
||||||
set(value) {
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, value)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
var hapticFeedback: Boolean
|
|
||||||
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, false)
|
|
||||||
set(value) {
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, value)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
var showFps: Boolean
|
|
||||||
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
|
|
||||||
set(value) {
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, value)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
var showOverlay: Boolean
|
|
||||||
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, true)
|
|
||||||
set(value) {
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, value)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,6 +22,7 @@ import java.io.BufferedOutputStream
|
|||||||
import java.lang.NullPointerException
|
import java.lang.NullPointerException
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
import kotlin.IllegalStateException
|
||||||
|
|
||||||
object FileUtil {
|
object FileUtil {
|
||||||
const val PATH_TREE = "tree"
|
const val PATH_TREE = "tree"
|
||||||
@ -342,6 +343,37 @@ object FileUtil {
|
|||||||
return TaskState.Completed
|
return TaskState.Completed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that copies the contents of a DocumentFile folder into a [File]
|
||||||
|
* @param file [File] representation of the folder to copy into
|
||||||
|
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
|
||||||
|
*/
|
||||||
|
fun DocumentFile.copyFilesTo(file: File) {
|
||||||
|
file.mkdirs()
|
||||||
|
if (!this.isDirectory || !file.isDirectory) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"[FileUtil] Tried to copy a folder into a file or vice versa"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listFiles().forEach {
|
||||||
|
val newFile = File(file, it.name!!)
|
||||||
|
if (it.isDirectory) {
|
||||||
|
newFile.mkdirs()
|
||||||
|
DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile)
|
||||||
|
} else {
|
||||||
|
val inputStream =
|
||||||
|
YuzuApplication.appContext.contentResolver.openInputStream(it.uri)
|
||||||
|
BufferedInputStream(inputStream).use { bos ->
|
||||||
|
if (!newFile.exists()) {
|
||||||
|
newFile.createNewFile()
|
||||||
|
}
|
||||||
|
newFile.outputStream().use { os -> bos.copyTo(os) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun isRootTreeUri(uri: Uri): Boolean {
|
fun isRootTreeUri(uri: Uri): Boolean {
|
||||||
val paths = uri.pathSegments
|
val paths = uri.pathSegments
|
||||||
return paths.size == 2 && PATH_TREE == paths[0]
|
return paths.size == 2 && PATH_TREE == paths[0]
|
||||||
|
@ -36,6 +36,12 @@ object GameHelper {
|
|||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
|
||||||
|
// Reset metadata so we don't use stale information
|
||||||
|
GameMetadata.resetMetadata()
|
||||||
|
|
||||||
|
// Remove previous filesystem provider information so we can get up to date version info
|
||||||
|
NativeLibrary.clearFilesystemProvider()
|
||||||
|
|
||||||
val badDirs = mutableListOf<Int>()
|
val badDirs = mutableListOf<Int>()
|
||||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||||
val gameDirUri = Uri.parse(gameDir.uriString)
|
val gameDirUri = Uri.parse(gameDir.uriString)
|
||||||
@ -92,14 +98,24 @@ object GameHelper {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
|
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
|
||||||
games.add(getGame(it.uri, true))
|
val game = getGame(it.uri, true)
|
||||||
|
if (game != null) {
|
||||||
|
games.add(game)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
|
fun getGame(uri: Uri, addedToLibrary: Boolean): Game? {
|
||||||
val filePath = uri.toString()
|
val filePath = uri.toString()
|
||||||
|
if (!GameMetadata.getIsValid(filePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needed to update installed content information
|
||||||
|
NativeLibrary.addFileToFilesystemProvider(filePath)
|
||||||
|
|
||||||
var name = GameMetadata.getTitle(filePath)
|
var name = GameMetadata.getTitle(filePath)
|
||||||
|
|
||||||
// If the game's title field is empty, use the filename.
|
// If the game's title field is empty, use the filename.
|
||||||
@ -118,7 +134,7 @@ object GameHelper {
|
|||||||
filePath,
|
filePath,
|
||||||
programId,
|
programId,
|
||||||
GameMetadata.getDeveloper(filePath),
|
GameMetadata.getDeveloper(filePath),
|
||||||
GameMetadata.getVersion(filePath),
|
GameMetadata.getVersion(filePath, false),
|
||||||
GameMetadata.getIsHomebrew(filePath)
|
GameMetadata.getIsHomebrew(filePath)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,13 +4,15 @@
|
|||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
object GameMetadata {
|
object GameMetadata {
|
||||||
|
external fun getIsValid(path: String): Boolean
|
||||||
|
|
||||||
external fun getTitle(path: String): String
|
external fun getTitle(path: String): String
|
||||||
|
|
||||||
external fun getProgramId(path: String): String
|
external fun getProgramId(path: String): String
|
||||||
|
|
||||||
external fun getDeveloper(path: String): String
|
external fun getDeveloper(path: String): String
|
||||||
|
|
||||||
external fun getVersion(path: String): String
|
external fun getVersion(path: String, reload: Boolean): String
|
||||||
|
|
||||||
external fun getIcon(path: String): ByteArray
|
external fun getIcon(path: String): ByteArray
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ import java.io.File
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.util.zip.ZipException
|
import java.util.zip.ZipException
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
@ -44,7 +46,7 @@ object GpuDriverHelper {
|
|||||||
NativeLibrary.initializeGpuDriver(
|
NativeLibrary.initializeGpuDriver(
|
||||||
hookLibPath,
|
hookLibPath,
|
||||||
driverInstallationPath,
|
driverInstallationPath,
|
||||||
customDriverData.libraryName,
|
installedCustomDriverData.libraryName,
|
||||||
fileRedirectionPath
|
fileRedirectionPath
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -190,6 +192,7 @@ object GpuDriverHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: ZipException) {
|
} catch (_: ZipException) {
|
||||||
|
} catch (_: FileNotFoundException) {
|
||||||
}
|
}
|
||||||
return GpuDriverMetadata()
|
return GpuDriverMetadata()
|
||||||
}
|
}
|
||||||
@ -197,9 +200,12 @@ object GpuDriverHelper {
|
|||||||
external fun supportsCustomDriverLoading(): Boolean
|
external fun supportsCustomDriverLoading(): Boolean
|
||||||
|
|
||||||
// Parse the custom driver metadata to retrieve the name.
|
// Parse the custom driver metadata to retrieve the name.
|
||||||
val customDriverData: GpuDriverMetadata
|
val installedCustomDriverData: GpuDriverMetadata
|
||||||
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
|
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
|
||||||
|
|
||||||
|
val customDriverSettingData: GpuDriverMetadata
|
||||||
|
get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString()))
|
||||||
|
|
||||||
fun initializeDirectories() {
|
fun initializeDirectories() {
|
||||||
// Ensure the file redirection directory exists.
|
// Ensure the file redirection directory exists.
|
||||||
val fileRedirectionDir = File(fileRedirectionPath!!)
|
val fileRedirectionDir = File(fileRedirectionPath!!)
|
||||||
|
@ -27,13 +27,13 @@ object MemoryUtil {
|
|||||||
const val Pb = Tb * 1024
|
const val Pb = Tb * 1024
|
||||||
const val Eb = Pb * 1024
|
const val Eb = Pb * 1024
|
||||||
|
|
||||||
private fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
|
fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
|
||||||
when {
|
when {
|
||||||
size < Kb -> {
|
size < Kb -> {
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.memory_formatted,
|
R.string.memory_formatted,
|
||||||
size.hundredths,
|
size.hundredths,
|
||||||
context.getString(R.string.memory_byte)
|
context.getString(R.string.memory_byte_shorthand)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
size < Mb -> {
|
size < Mb -> {
|
||||||
|
@ -4,59 +4,117 @@
|
|||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
import org.yuzu.yuzu_emu.model.GameDir
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
|
||||||
|
|
||||||
object NativeConfig {
|
object NativeConfig {
|
||||||
/**
|
/**
|
||||||
* Creates a Config object and opens the emulation config.
|
* Loads global config.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun initializeConfig()
|
external fun initializeGlobalConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys the stored config object. This automatically saves the existing config.
|
* Destroys the stored global config object. This does not save the existing config.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun unloadConfig()
|
external fun unloadGlobalConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads values saved to the config file and saves them.
|
* Reads values in the global config file and saves them.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun reloadSettings()
|
external fun reloadGlobalConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves settings values in memory to disk.
|
* Saves global settings values in memory to disk.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun saveSettings()
|
external fun saveGlobalConfig()
|
||||||
|
|
||||||
external fun getBoolean(key: String, getDefault: Boolean): Boolean
|
/**
|
||||||
|
* Creates per-game config for the specified parameters. Must be unloaded once per-game config
|
||||||
|
* is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets
|
||||||
|
* will follow the per-game config until the global config is reloaded.
|
||||||
|
*
|
||||||
|
* @param programId String representation of the u64 programId
|
||||||
|
* @param fileName Filename of the game, including its extension
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun initializePerGameConfig(programId: String, fileName: String)
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
external fun isPerGameConfigLoaded(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves per-game settings values in memory to disk.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun savePerGameConfig()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the stored per-game config object. This does not save the config.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun unloadPerGameConfig()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
external fun getBoolean(key: String, needsGlobal: Boolean): Boolean
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
external fun setBoolean(key: String, value: Boolean)
|
external fun setBoolean(key: String, value: Boolean)
|
||||||
|
|
||||||
external fun getByte(key: String, getDefault: Boolean): Byte
|
@Synchronized
|
||||||
|
external fun getByte(key: String, needsGlobal: Boolean): Byte
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
external fun setByte(key: String, value: Byte)
|
external fun setByte(key: String, value: Byte)
|
||||||
|
|
||||||
external fun getShort(key: String, getDefault: Boolean): Short
|
@Synchronized
|
||||||
|
external fun getShort(key: String, needsGlobal: Boolean): Short
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
external fun setShort(key: String, value: Short)
|
external fun setShort(key: String, value: Short)
|
||||||
|
|
||||||
external fun getInt(key: String, getDefault: Boolean): Int
|
@Synchronized
|
||||||
|
external fun getInt(key: String, needsGlobal: Boolean): Int
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
external fun setInt(key: String, value: Int)
|
external fun setInt(key: String, value: Int)
|
||||||
|
|
||||||
external fun getFloat(key: String, getDefault: Boolean): Float
|
@Synchronized
|
||||||
|
external fun getFloat(key: String, needsGlobal: Boolean): Float
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
external fun setFloat(key: String, value: Float)
|
external fun setFloat(key: String, value: Float)
|
||||||
|
|
||||||
external fun getLong(key: String, getDefault: Boolean): Long
|
@Synchronized
|
||||||
|
external fun getLong(key: String, needsGlobal: Boolean): Long
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
external fun setLong(key: String, value: Long)
|
external fun setLong(key: String, value: Long)
|
||||||
|
|
||||||
external fun getString(key: String, getDefault: Boolean): String
|
@Synchronized
|
||||||
|
external fun getString(key: String, needsGlobal: Boolean): String
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
external fun setString(key: String, value: String)
|
external fun setString(key: String, value: String)
|
||||||
|
|
||||||
external fun getIsRuntimeModifiable(key: String): Boolean
|
external fun getIsRuntimeModifiable(key: String): Boolean
|
||||||
|
|
||||||
external fun getConfigHeader(category: Int): String
|
|
||||||
|
|
||||||
external fun getPairedSettingKey(key: String): String
|
external fun getPairedSettingKey(key: String): String
|
||||||
|
|
||||||
|
external fun getIsSwitchable(key: String): Boolean
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
external fun usingGlobal(key: String): Boolean
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
external fun setGlobal(key: String, global: Boolean)
|
||||||
|
|
||||||
|
external fun getIsSaveable(key: String): Boolean
|
||||||
|
|
||||||
|
external fun getDefaultToString(key: String): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets every [GameDir] in AndroidSettings::values.game_dirs
|
* Gets every [GameDir] in AndroidSettings::values.game_dirs
|
||||||
*/
|
*/
|
||||||
@ -74,4 +132,40 @@ object NativeConfig {
|
|||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun addGameDir(dir: GameDir)
|
external fun addGameDir(dir: GameDir)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an array of the addons that are disabled for a given game
|
||||||
|
*
|
||||||
|
* @param programId String representation of a game's program ID
|
||||||
|
* @return An array of disabled addons
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun getDisabledAddons(programId: String): Array<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the disabled addons array corresponding to [programId] and replaces them
|
||||||
|
* with [disabledAddons]
|
||||||
|
*
|
||||||
|
* @param programId String representation of a game's program ID
|
||||||
|
* @param disabledAddons Replacement array of disabled addons
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun setDisabledAddons(programId: String, disabledAddons: Array<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an array of [OverlayControlData] from settings
|
||||||
|
*
|
||||||
|
* @return An array of [OverlayControlData]
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun getOverlayControlData(): Array<OverlayControlData>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the AndroidSettings::values.overlay_control_data array and replaces its values
|
||||||
|
* with [overlayControlData]
|
||||||
|
*
|
||||||
|
* @param overlayControlData Replacement array of [OverlayControlData]
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
object PreferenceUtil {
|
||||||
|
/**
|
||||||
|
* Retrieves a shared preference value and then deletes the value in storage.
|
||||||
|
* @param key Associated key for the value in this preferences instance
|
||||||
|
* @return Typed value associated with [key]. Null if no such key exists.
|
||||||
|
*/
|
||||||
|
inline fun <reified T> SharedPreferences.migratePreference(key: String): T? {
|
||||||
|
if (!this.contains(key)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val value: Any = when (T::class) {
|
||||||
|
String::class -> this.getString(key, "")!!
|
||||||
|
|
||||||
|
Boolean::class -> this.getBoolean(key, false)
|
||||||
|
|
||||||
|
Int::class -> this.getInt(key, 0)
|
||||||
|
|
||||||
|
Float::class -> this.getFloat(key, 0f)
|
||||||
|
|
||||||
|
Long::class -> this.getLong(key, 0)
|
||||||
|
|
||||||
|
else -> throw IllegalStateException("Tried to migrate preference with invalid type!")
|
||||||
|
}
|
||||||
|
deletePreference(key)
|
||||||
|
return value as T
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SharedPreferences.deletePreference(key: String) = this.edit().remove(key).apply()
|
||||||
|
}
|
@ -5,38 +5,38 @@ package org.yuzu.yuzu_emu.utils
|
|||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||||
import org.yuzu.yuzu_emu.ui.main.ThemeProvider
|
import org.yuzu.yuzu_emu.ui.main.ThemeProvider
|
||||||
|
|
||||||
object ThemeHelper {
|
object ThemeHelper {
|
||||||
const val SYSTEM_BAR_ALPHA = 0.9f
|
const val SYSTEM_BAR_ALPHA = 0.9f
|
||||||
|
|
||||||
private const val DEFAULT = 0
|
|
||||||
private const val MATERIAL_YOU = 1
|
|
||||||
|
|
||||||
fun setTheme(activity: AppCompatActivity) {
|
fun setTheme(activity: AppCompatActivity) {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
|
||||||
setThemeMode(activity)
|
setThemeMode(activity)
|
||||||
when (preferences.getInt(Settings.PREF_THEME, 0)) {
|
when (Theme.from(IntSetting.THEME.getInt())) {
|
||||||
DEFAULT -> activity.setTheme(R.style.Theme_Yuzu_Main)
|
Theme.Default -> activity.setTheme(R.style.Theme_Yuzu_Main)
|
||||||
MATERIAL_YOU -> activity.setTheme(R.style.Theme_Yuzu_Main_MaterialYou)
|
Theme.MaterialYou -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
activity.setTheme(R.style.Theme_Yuzu_Main_MaterialYou)
|
||||||
|
} else {
|
||||||
|
activity.setTheme(R.style.Theme_Yuzu_Main)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using a specific night mode check because this could apply incorrectly when using the
|
// Using a specific night mode check because this could apply incorrectly when using the
|
||||||
// light app mode, dark system mode, and black backgrounds. Launching the settings activity
|
// light app mode, dark system mode, and black backgrounds. Launching the settings activity
|
||||||
// will then show light mode colors/navigation bars but with black backgrounds.
|
// will then show light mode colors/navigation bars but with black backgrounds.
|
||||||
if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) &&
|
if (BooleanSetting.BLACK_BACKGROUNDS.getBoolean() && isNightMode(activity)) {
|
||||||
isNightMode(activity)
|
|
||||||
) {
|
|
||||||
activity.setTheme(R.style.ThemeOverlay_Yuzu_Dark)
|
activity.setTheme(R.style.ThemeOverlay_Yuzu_Dark)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,8 +60,7 @@ object ThemeHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setThemeMode(activity: AppCompatActivity) {
|
fun setThemeMode(activity: AppCompatActivity) {
|
||||||
val themeMode = PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
val themeMode = IntSetting.THEME_MODE.getInt()
|
||||||
.getInt(Settings.PREF_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
|
||||||
activity.delegate.localNightMode = themeMode
|
activity.delegate.localNightMode = themeMode
|
||||||
val windowController = WindowCompat.getInsetsController(
|
val windowController = WindowCompat.getInsetsController(
|
||||||
activity.window,
|
activity.window,
|
||||||
@ -95,3 +94,12 @@ object ThemeHelper {
|
|||||||
windowController.isAppearanceLightNavigationBars = false
|
windowController.isAppearanceLightNavigationBars = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class Theme(val int: Int) {
|
||||||
|
Default(0),
|
||||||
|
MaterialYou(1);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(int: Int): Theme = entries.firstOrNull { it.int == int } ?: Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
std::string GetJString(JNIEnv* env, jstring jstr) {
|
std::string GetJString(JNIEnv* env, jstring jstr) {
|
||||||
if (!jstr) {
|
if (!jstr) {
|
||||||
@ -33,3 +34,11 @@ jstring ToJString(JNIEnv* env, std::string_view str) {
|
|||||||
jstring ToJString(JNIEnv* env, std::u16string_view str) {
|
jstring ToJString(JNIEnv* env, std::u16string_view str) {
|
||||||
return ToJString(env, Common::UTF16ToUTF8(str));
|
return ToJString(env, Common::UTF16ToUTF8(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double GetJDouble(JNIEnv* env, jobject jdouble) {
|
||||||
|
return env->GetDoubleField(jdouble, IDCache::GetDoubleValueField());
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject ToJDouble(JNIEnv* env, double value) {
|
||||||
|
return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
|
||||||
|
}
|
||||||
|
@ -10,3 +10,6 @@
|
|||||||
std::string GetJString(JNIEnv* env, jstring jstr);
|
std::string GetJString(JNIEnv* env, jstring jstr);
|
||||||
jstring ToJString(JNIEnv* env, std::string_view str);
|
jstring ToJString(JNIEnv* env, std::string_view str);
|
||||||
jstring ToJString(JNIEnv* env, std::u16string_view str);
|
jstring ToJString(JNIEnv* env, std::u16string_view str);
|
||||||
|
|
||||||
|
double GetJDouble(JNIEnv* env, jobject jdouble);
|
||||||
|
jobject ToJDouble(JNIEnv* env, double value);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user