Compare commits

...

197 Commits

Author SHA1 Message Date
28a23c4380 Android 215 2024-01-28 01:00:30 +00:00
c768c19898 Merge yuzu-emu#12802 2024-01-28 01:00:30 +00:00
7273701bc2 Merge yuzu-emu#12760 2024-01-28 01:00:30 +00:00
3bf0492edb Merge yuzu-emu#12749 2024-01-28 01:00:30 +00:00
ce2eb6e8ee Merge pull request #12818 from K900/small-fixes
A few small fixes
2024-01-27 12:13:46 -05:00
8b47465586 input: add a missing null pointer check
There's a few other places where the result of GetAruidData is accessed without a null check,
but I couldn't find a code path that hits those.
2024-01-27 17:32:49 +03:00
3065ab0fd8 nx_tzdb: add another safety assertion 2024-01-27 17:28:04 +03:00
a2407a2964 nx_tzdb: check for unpacked directory
Otherwise things get funny if the archive is downloaded, but the unpacking was interrupted.
2024-01-27 17:25:52 +03:00
16b79df836 Merge pull request #12815 from t895/visual-driver-silly
android: Reload global settings on closing emulation
2024-01-27 01:36:26 -05:00
6a4b25699d android: Reload global settings on closing emulation
UI like the driver manager expects the global settings to be loaded when in the MainActivity so we reload global config to properly reset state on exit.
2024-01-26 23:05:02 -05:00
bd8635e26a Merge pull request #12808 from t895/uri-moment
vfs: Fix getting URI filename
2024-01-26 10:23:08 -05:00
4349cdba07 Merge pull request #12769 from german77/no-log
core: hid: Reduce controller requests
2024-01-26 09:57:40 -05:00
f2fb761bac vfs: Fix getting URI filename 2024-01-26 09:57:22 -05:00
59aee2b461 Merge pull request #12809 from t895/error-message
android: Add cancel condition to installed content check
2024-01-26 09:56:01 -05:00
4d206d849e Merge pull request #12765 from german77/sys-hid
service: set: Implement more settings functions for Qlaunch
2024-01-26 09:55:47 -05:00
744c0173d1 Merge pull request #12801 from german77/vibration-fix
service: hid: Don't try to vibrate if device isn't initialized
2024-01-26 09:55:37 -05:00
55482ab5dc Merge pull request #12707 from FearlessTobi/fs-housekeeping
fs: Various cleanups & add path class for later use
2024-01-26 09:55:25 -05:00
e56b44dee6 android: Add cancel condition to installed content check 2024-01-26 09:29:51 -05:00
f2012e5aff service: hid: Don't try to vibrate if device isn't initialized 2024-01-25 23:46:46 -06:00
4526fdaf64 Merge pull request #12796 from t895/controller-optimizations
android: Controller focus optimizations
2024-01-25 23:01:44 -05:00
bc22b4e782 Merge pull request #12798 from liamwhite/cmif-fixes
cmif_serialization: fixes
2024-01-25 23:01:10 -05:00
f70821ce0d Merge pull request #12794 from abouvier/cmake-simpleini-module
cmake: support simpleini config and pc file
2024-01-25 23:01:04 -05:00
a774ff935c cmif_serialization: support non-domain sessions on domain servers 2024-01-25 22:18:42 -05:00
431df5ae93 cmif_types: improve ergonomics of types 2024-01-25 22:18:37 -05:00
677c2c2cd2 android: Disable default focus highlight on views that shouldn't be selected 2024-01-25 20:49:57 -05:00
ee540c712c android: Allow controller to focus on toolbar menu items
Workaround for this
https://issuetracker.google.com/issues/256948272
2024-01-25 20:48:58 -05:00
d23c4393fd android: Add 600dp layout for GameInfoFragment 2024-01-25 20:46:03 -05:00
b24a111136 android: Fix button click listener for build version name
Was set to the text instead of the parent view by mistake
2024-01-25 20:43:12 -05:00
91636deaaf android: Disable focus for the root of cards that contain buttons 2024-01-25 20:42:23 -05:00
68cbf67f4c android: Focus on the in game menu when opened 2024-01-25 20:39:52 -05:00
645961613f cmake: support simpleini cmake config and pc file 2024-01-26 01:13:47 +01:00
53b321c945 service: set: Implement more settings functions for Qlaunch 2024-01-25 17:14:18 -06:00
975deb7528 Address review comments and fix compilation problems 2024-01-25 16:43:53 -05:00
2c049ae06d fs: Add path class 2024-01-25 16:42:06 -05:00
54372fdff5 result: Make fully constexpr, add ON_RESULT_INCLUDED 2024-01-25 16:42:06 -05:00
c60ab6bbf6 fs/errors: Unify naming of result codes 2024-01-25 16:42:06 -05:00
cc09c265e1 fs: Replace Mode enum by OpenMode enum 2024-01-25 16:42:05 -05:00
0f9288e38d vfs: Move vfs files to their own directory 2024-01-25 16:40:42 -05:00
06fb7f90da fs: Move fsp_srv subclasses to separate files
fs: Move additional files to the fsp directory
2024-01-25 16:40:42 -05:00
e04368ad7c Merge pull request #12759 from liamwhite/mp-misc
core: miscellaneous fixes
2024-01-25 16:21:38 -05:00
3e2d3548f2 Merge pull request #12777 from t895/firmware-warning
android: Add key warning
2024-01-25 16:21:29 -05:00
eb9036d75b Merge pull request #12783 from liamwhite/cmif-generation
service: add template serializer for method calls
2024-01-25 15:40:09 -05:00
01a2d978eb service: add template serializer for method calls 2024-01-25 14:35:51 -05:00
6e67b25af9 Merge pull request #12787 from t895/game-list-refresh
android: Only compare game contents for GameAdapter
2024-01-25 14:19:32 -05:00
e91667ba75 Merge pull request #12786 from t895/driver-overlay
android: Show driver vendor in FPS overlay
2024-01-25 14:19:25 -05:00
d45561ace0 Merge pull request #12499 from Kelebek1/time
Rework time services
2024-01-25 14:19:01 -05:00
0fdd6e8934 android: Fix waiting for driver install on startup 2024-01-25 13:04:04 -05:00
35794f4f18 android: Add current driver vendor to FPS overlay 2024-01-25 13:04:03 -05:00
b8be8dff69 android: Add key check 2024-01-25 12:58:19 -05:00
bc317a9807 android: Add option to make MessageDialogFragments non-dismissible
Additionally fixes an issue where its viewmodel could hold onto a stale positive action
2024-01-25 12:53:49 -05:00
97ca160b08 frontend_common: Consistently use references
Was swapping between references and pointers for no reason. Just unify them here since each of these utility functions will need their parameters to be alive.
2024-01-25 12:53:49 -05:00
1a3fc3724a frontend_common: Remove key rederivation and keep key check 2024-01-25 12:53:48 -05:00
7b01454d5f android: Only compare game contents for GameAdapter 2024-01-25 08:04:59 -05:00
f3749394ac Merge pull request #12781 from goldenx86/dozen
Demote dozen to the bottom of the device list
2024-01-25 03:58:09 -03:00
807f421752 Demote Mesa dozen to the bottom of the device list 2024-01-24 23:36:14 -03:00
e4915fb7d2 Rework time service to fix time passing offline. 2024-01-24 04:26:55 +00:00
a76f6a2775 Merge pull request #12763 from liamwhite/fix-hbl-again
loader: also register fs process for raw exefs partition
2024-01-23 13:31:41 -05:00
ba518f6899 Merge pull request #12768 from german77/wrong_conversion
service: properly convert buffers to strings
2024-01-23 13:31:27 -05:00
ad4622da2c core: hid: Skip duplicated vibrations 2024-01-23 11:33:08 -06:00
3b1c2896d9 core: hid: Only set polling mode if needed 2024-01-23 11:11:09 -06:00
fc5d76e6e2 service: properly convert buffers to strings 2024-01-23 10:24:05 -06:00
5f9a45ada9 loader: also register fs process for raw exefs partition 2024-01-23 00:01:38 -05:00
a120f8ff4d nvservices: close map handles on session close 2024-01-22 21:18:52 -05:00
96833cd809 kernel: target invalidate to given process 2024-01-22 21:18:52 -05:00
8649a80071 Merge pull request #12753 from liamwhite/why
device_memory_manager: fix ScratchBuffer indexing
2024-01-22 14:55:07 -05:00
550cadbee4 device_memory_manager: fix ScratchBuffer indexing 2024-01-22 14:07:33 -05:00
8bd10473d6 Merge pull request #12579 from FernandoS27/smmu
Core: Implement Device Mapping & GPU SMMU
2024-01-22 10:55:39 -05:00
8d708b0c79 Merge pull request #12747 from t895/homescreen-widget
android: Add to launcher button
2024-01-22 10:55:25 -05:00
beaab10c8f android: Add to launcher button 2024-01-22 08:19:20 -05:00
889c5d2705 Merge pull request #12739 from t895/debug-keystore
android: Provide debug.keystore for debug and relWithDebInfo builds
2024-01-22 04:34:17 -05:00
17b0aac809 Merge pull request #12738 from t895/lock-drawer
android: Port "Lock drawer" feature from Citra
2024-01-22 04:34:08 -05:00
399220ddbc Merge pull request #12736 from t895/verify-contents
android: Add verify contents buttons
2024-01-22 04:33:56 -05:00
59080a3d1d android: Provide debug.keystore for debug and relWithDebInfo builds
Allows devs to share debug builds with testers without uninstalling the previous build
2024-01-21 22:08:07 -05:00
3a25a217e6 android: Port "Lock drawer" feature from Citra 2024-01-21 20:47:28 -05:00
961b5586a5 frontend_common: Remove default value for installer callbacks
We never used these without callbacks and these will break without them in their current state. I could write the default value to return false always but that's unnecessary for now.
2024-01-21 19:31:26 -05:00
57ff934f0d Merge pull request #12734 from german77/enable-applet
service: hid: Implement EnableAppletForInput
2024-01-21 19:15:53 -05:00
92ce9273ee Merge pull request #12735 from german77/disable-vibration
core: hid: Allow to disable vibration
2024-01-21 19:15:46 -05:00
dd36d43ea1 android: Add options to verify installed content 2024-01-21 19:15:11 -05:00
a7a7720752 core: hid: Allow to disable vibration 2024-01-21 16:44:31 -06:00
c725f3c86c frontend_common: Move integrity verification to content_manager 2024-01-21 16:36:37 -05:00
1b984738ab service: hid: Implement EnableAppletForInput 2024-01-21 14:05:18 -06:00
a3199401f4 Merge pull request #12733 from german77/settings_services
service: set: Don't allow invalid mii author id
2024-01-21 14:08:58 -05:00
a7620a29be service: set: Don't allow invalid mii author id 2024-01-21 12:18:18 -06:00
5ac1297fa5 Merge pull request #12728 from t895/sync-translations
android: Sync translations
2024-01-21 00:38:49 -05:00
fe69105f71 android: Sync translations 2024-01-20 23:26:47 -05:00
93a3342841 Merge pull request #12720 from t895/return-to-global
android: Change "Clear" to "Use global setting" for per-game settings
2024-01-20 13:56:31 -05:00
7b3e26acc9 android: Change "Clear" to "Use global setting" for per-game settings 2024-01-20 13:37:47 -05:00
444e86d191 Merge pull request #12688 from liamwhite/wl-present-fix
renderer_vulkan: recreate swapchain when frame size changes
2024-01-20 13:36:18 -05:00
61ce0088ae Merge pull request #12724 from merryhime/fs-u8str-overloads
fs/file: Explicitly convert std::u8string to std::filesystem::path
2024-01-20 13:35:41 -05:00
b3aa3633c7 Merge pull request #12721 from t895/card-elevation
android: Use elevated card style for home setting card
2024-01-20 13:35:30 -05:00
627ba271ad Merge pull request #12719 from t895/sort-search
android: Sort recently added/played games by time
2024-01-20 13:35:14 -05:00
2faa631676 Merge pull request #12715 from t895/remove-addons
android: Add uninstall addon button
2024-01-20 13:35:03 -05:00
5838779162 Merge pull request #12660 from german77/better-vibration
service: hid: Fully implement abstract vibration
2024-01-20 13:34:54 -05:00
23fd1041c1 Merge pull request #12701 from liamwhite/flinger-layer-issues
vi: check layer state before opening or closing
2024-01-20 13:34:32 -05:00
5c398ede6f fs/file: Explicitly convert std::u8string to std::filesystem::path 2024-01-20 17:46:30 +00:00
378e4752a6 android: Use elevated card style for home setting card 2024-01-20 03:55:48 -05:00
dad48f16b7 android: Sort recently added/played games by time 2024-01-20 03:18:48 -05:00
a363fa78ef frontend_common: Add documentation for content_mananger 2024-01-19 20:54:50 -05:00
03fa91ba3c android: Add addon delete button
Required some refactoring of retrieving patches in order for the frontend to pass the right information to ContentManager for deletion.
2024-01-19 20:54:50 -05:00
d79d4d5986 android: Use callback to update progress bar dialogs 2024-01-19 17:09:36 -05:00
ccd3dd842f frontend_common: Add content manager utility functions
Creates utility functions to remove/install DLC, updates, and base game content
2024-01-19 17:09:35 -05:00
b4a8e1ef8a Merge pull request #12713 from shinra-electric/mvk-127
macOS: Bump MoltenVK to v1.2.7
2024-01-19 13:07:14 -05:00
5ea8f05ec6 Bump MoltenVK to v1.2.7 2024-01-19 17:28:53 +01:00
10535e0016 Merge pull request #12687 from german77/amiibo-lock
core: hid: Disable special features before disconnecting the controllers
2024-01-19 09:33:31 -05:00
a8c552e261 Merge pull request #12695 from anpilley/user-arguments-v2
Allow -u to accept a username string in addition to index
2024-01-19 09:33:25 -05:00
932bd98824 Merge pull request #12709 from german77/npad-disc
service: hid: Clear controller status when aruid is no longer used
2024-01-19 09:33:16 -05:00
9f376cd901 service: hid: Clear controller status when aruid is no longer used 2024-01-19 00:09:49 -06:00
a560b9f5a2 Merge pull request #12678 from german77/settings_impl
service: set: Implement stubbed functions
2024-01-18 21:18:37 -05:00
4f04bd3697 Merge pull request #12683 from german77/amiibo-dump
service: nfc: Create backup when none exist
2024-01-18 21:18:27 -05:00
97c8b49444 Merge pull request #12644 from liamwhite/vkspec-image-offset
shader_recompiler: fix Offset operand usage for non-OpImage*Gather
2024-01-18 21:18:19 -05:00
748465f5a5 device_memory_manager: use unique_lock for update 2024-01-18 21:12:30 -05:00
04867e2456 nvhost_vic: use map erase by key 2024-01-18 21:12:30 -05:00
32f623e029 nvdrv: clean up preallocation 2024-01-18 21:12:30 -05:00
b6c6534c30 nvdrv: use correct names for interface factory 2024-01-18 21:12:30 -05:00
beb438bb0b nvdrv: use static typing for SessionId, smmu Asid types 2024-01-18 21:12:30 -05:00
4b963ca8a5 Core: Invert guest memory depandancy 2024-01-18 21:12:30 -05:00
648ed55fe6 Core: Make sure GPU Dirty Managers ae shared by all processes. 2024-01-18 21:12:30 -05:00
23430e6772 Core: Eliminate core/memory dependancies. 2024-01-18 21:12:30 -05:00
0672847330 SMMU: Fix Right Shift UB. 2024-01-18 21:12:30 -05:00
a874ab0133 SMMU: Fix 8Gb layout. 2024-01-18 21:12:30 -05:00
590d9b7e1d Core: Clang format and other small issues. 2024-01-18 21:12:30 -05:00
b0bca0f8b0 SMMU: Fix software rendering and cleanup 2024-01-18 21:12:30 -05:00
d8f1ce2f76 SMMU: Add continuity tracking optimization. 2024-01-18 21:12:30 -05:00
9b11b9dce5 SMMU: Simplify and remove old code. 2024-01-18 21:12:30 -05:00
303cd31162 SMMU: Add Android compatibility 2024-01-18 21:12:30 -05:00
0adc09e0af GPU-SMMU: Estimate game leak and preallocate device region. 2024-01-18 21:12:30 -05:00
96fd1348ae GPU SMMU: Expand to 34 bits 2024-01-18 21:12:30 -05:00
bad705f245 SMMU: Fix Unregister on MultiAddress 2024-01-18 21:12:30 -05:00
34a8d0cc8e SMMU: Implement physical memory mirroring 2024-01-18 21:12:30 -05:00
0a2536a0df SMMU: Initial adaptation to video_core. 2024-01-18 21:12:30 -05:00
c85d7ccd79 SMMU: Implement backing CPU page protect/unprotect 2024-01-18 21:12:30 -05:00
7a9d1ad2f8 NVDRV: Implement sessions and initial implementation of SMMU 2024-01-18 21:12:30 -05:00
2f0418c101 Core: Initial implementation of device memory mapping 2024-01-18 21:12:30 -05:00
3092855d5a Merge pull request #12702 from german77/android-input
input_common: Add android input engine
2024-01-18 09:16:58 -05:00
72f803c366 input_common: Add android input engine 2024-01-17 22:47:56 -06:00
c87b96435d Merge pull request #12699 from t895/overlay-saving
android: Save overlay data while using emulation fragment
2024-01-17 22:56:40 -05:00
e4bbb24dcf vi: check layer state before opening or closing 2024-01-17 22:03:40 -05:00
6536d29c61 Update based on feedback 2024-01-17 18:14:05 -08:00
116f76e4b6 android: Save overlay data while using emulation fragment
This should have been fully embraced before but the items within the popup menu and the adjust controls dialog fell through. This ensures that everything related to the overlay is saved during emulation and can't be lost during a crash.
2024-01-17 20:14:25 -05:00
ce89580749 nvnflinger: ensure display abandonment considers all layers and future layers 2024-01-17 18:45:39 -05:00
dff0a7c52a Allow -u to accept a username string in addition to index, and suppress the User selector even if settings requires it to be shown for one instance only. 2024-01-17 10:31:00 -08:00
915efa4236 Merge pull request #12689 from liamwhite/remove-format
ci: remove format dep from mainline step2
2024-01-17 00:36:07 -05:00
4548e5ae1d ci: remove format dep from mainline step2 2024-01-16 22:59:20 -05:00
46c2435235 Merge pull request #12380 from flodavid/save-profile
Save configuration profile name used by players
2024-01-16 21:27:25 -06:00
e9eb017aac renderer_vulkan: recreate swapchain when frame size changes 2024-01-16 16:09:39 -05:00
0b0e9ef18d core: hid: Disable special features before disconnecting the controllers 2024-01-16 14:44:54 -06:00
7f5adf8982 service: set: Implement stubbed functions 2024-01-15 23:17:03 -06:00
89d6856090 service: set: Refractor setting service 2024-01-15 23:16:36 -06:00
2cacb9d48c service: hid: Fully implement abstract vibration 2024-01-15 23:15:40 -06:00
2c29c2b8dd Merge pull request #12686 from szepeviktor/typos3
Fix more typos
2024-01-15 23:26:08 -05:00
16abda59be Fix typos in master 2024-01-16 00:09:25 +00:00
90ab89a0b0 Merge remote-tracking branch 'origin/master' into typos3 2024-01-16 00:09:00 +00:00
6531ad56a6 Fix typos in arrays.xml 2024-01-15 23:39:45 +00:00
e8671ed04e Fix one more typo 2024-01-15 23:34:11 +00:00
2044ae6b3a Fix more typos 2024-01-15 23:26:53 +00:00
c661b95864 service: nfc: Create backup when none exist 2024-01-15 14:07:54 -06:00
c683ec2bcb Merge pull request #12681 from t895/stick-toggles
android: Fix overlay toggle ordering
2024-01-15 13:52:53 -05:00
2e4e33156e Merge pull request #12680 from t895/format-mainline
ci: Remove format step from mainline builds
2024-01-15 13:52:48 -05:00
04f4eeaca2 Merge pull request #12677 from GPUCode/whyy-modders
core: Support multiple modules per patcher
2024-01-15 13:52:38 -05:00
2e4b32204c Merge pull request #12665 from german77/proof
service: acc: Only save profiles when profiles have changed
2024-01-15 13:52:33 -05:00
34db13486a Merge pull request #12659 from liamwhite/audio-memory
audio: fetch process object from handle table
2024-01-15 13:52:01 -05:00
c6c6bb4041 Merge pull request #12652 from liamwhite/huge-pile-of-spirv-spaghetti
shader_recompiler: emulate 8-bit and 16-bit storage writes with cas loop
2024-01-15 13:51:36 -05:00
a2ffb419c9 Merge pull request #12612 from liamwhite/fs-pid
fsp-srv: use program registry for SetCurrentProcess
2024-01-15 13:51:14 -05:00
0127cec371 Merge pull request #12611 from liamwhite/resource-management-is-hard
kernel: fix resource management issues
2024-01-15 13:50:58 -05:00
db3a6075f5 Merge pull request #12610 from liamwhite/reply-and-dont-receive
server_manager: respond to session close correctly
2024-01-15 13:50:43 -05:00
8876a15227 android: Fix overlay toggle ordering 2024-01-15 12:41:49 -05:00
954eb40237 ci: Remove format step from mainline builds 2024-01-15 10:30:57 -05:00
d4acdac168 core: Support multiple modules per patcher 2024-01-15 00:46:05 +02:00
817c7c445d Merge pull request #12667 from t895/version-info
android: Show version name instead of build hash in about fragment
2024-01-13 20:23:12 -05:00
da714a362b Merge pull request #12666 from t895/ktlint-yuzu-verify
android: Move ktlintCheck to yuzu-verify
2024-01-13 20:23:02 -05:00
7b3941e5d4 android: Show version name instead of git hash in the about fragment 2024-01-13 18:12:19 -05:00
15d8a40529 android: Clean up git commands in build.gradle 2024-01-13 18:06:33 -05:00
cdeaca73c4 android: Move ktlintCheck to yuzu-verify 2024-01-13 17:41:01 -05:00
bee22540a1 service: acc: Only save profiles when profiles have changed 2024-01-13 14:28:29 -06:00
76880b84f9 loader: fix homebrew nro registration 2024-01-13 13:48:56 -05:00
2f0b57ca13 kernel: optimize page free on shutdown 2024-01-12 19:19:07 -05:00
f90a022d3a kernel: fix debugger and process list lifetime 2024-01-12 18:31:33 -05:00
f2fed21c11 kernel: fix page leak on process termination 2024-01-12 18:31:33 -05:00
d940974789 audio: fetch process object from handle table 2024-01-12 10:03:16 -05:00
f7a3c135e2 Merge pull request #12605 from german77/abstract
service: hid: Create abstracted pad structure
2024-01-12 10:02:13 -05:00
fcb0dff67c Merge pull request #12642 from t895/adapter-refactor
android: Refactor list adapters
2024-01-12 10:01:54 -05:00
b5dac5f525 service: hid: Create abstracted pad structure 2024-01-11 19:35:04 -06:00
2a0d707ce1 shader_recompiler: emulate 8-bit and 16-bit storage writes with cas loop 2024-01-11 16:50:59 -05:00
aae9eea532 fsp-srv: use program registry for SetCurrentProcess 2024-01-11 11:28:52 -05:00
2044a289f8 shader_recompiler: fix Offset operand usage for non-OpImage*Gather 2024-01-11 00:56:37 -05:00
d3ba6b334b android: Fix added driver path
While this didn't break anything, the extra separator was unnecessary
2024-01-10 23:14:04 -05:00
dac8c4ce4d android: Add button to use global driver value 2024-01-10 23:14:04 -05:00
9e974d4c7e android: Reload driver data on importing user data 2024-01-10 23:14:04 -05:00
6bfc3c530c android: Rework driver fragment
Applies settings upon selection and uses a new Driver model to represent the information in-view. Also switches from an async diff list to a plain one.
2024-01-10 23:14:04 -05:00
93239f191a android: Refactor DriverAdapter to use AbstractSingleSelectionList 2024-01-10 23:14:04 -05:00
b17db2b462 android: Create generic single selection list adapter 2024-01-10 23:14:04 -05:00
9130366a58 android: Refactor recycler view adapters to use AbstractListAdapter 2024-01-10 23:14:04 -05:00
ad0066a6b6 android: Create generic list adapter for basic lists
Simplifies basic setup for lists
2024-01-10 23:14:04 -05:00
78c323c4eb android: Refactor async diff adapters to use AbstractDiffAdapter 2024-01-10 23:14:04 -05:00
51ad2d10de android: Create generic adapter and viewholder
Eliminates repeated code associated with every async differ list
2024-01-10 23:14:04 -05:00
63b835f822 Save profile name used
- Save the profile name in global config
- Read the profile name when reading the global config
2024-01-08 18:43:56 +01:00
200b371d13 server_manager: respond to session close correctly 2024-01-07 21:33:24 -05:00
782 changed files with 34234 additions and 15325 deletions

View File

@ -32,3 +32,6 @@ if [ ! -z "$DIFF" ]; then
echo "$DIFF" echo "$DIFF"
exit 1 exit 1
fi fi
cd src/android
./gradlew ktlintCheck

View File

@ -8,17 +8,7 @@ variables:
DisplayVersion: $[counter(variables['DisplayPrefix'], 1)] DisplayVersion: $[counter(variables['DisplayPrefix'], 1)]
stages: stages:
- stage: format
displayName: 'format'
jobs:
- job: format
displayName: 'clang'
pool:
vmImage: ubuntu-latest
steps:
- template: ./templates/format-check.yml
- stage: build - stage: build
dependsOn: format
displayName: 'build' displayName: 'build'
jobs: jobs:
- job: build - job: build
@ -43,7 +33,6 @@ stages:
cache: 'true' cache: 'true'
version: $(DisplayVersion) version: $(DisplayVersion)
- stage: build_win - stage: build_win
dependsOn: format
displayName: 'build-windows' displayName: 'build-windows'
jobs: jobs:
- job: build - job: build

View File

@ -13,13 +13,15 @@ jobs:
format: format:
name: 'verify format' name: 'verify format'
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: yuzuemu/build-environments:linux-clang-format
options: -u 1001
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
submodules: false submodules: false
- name: set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: 'Verify Formatting' - name: 'Verify Formatting'
run: bash -ex ./.ci/scripts/format/script.sh run: bash -ex ./.ci/scripts/format/script.sh
build: build:

View File

@ -155,3 +155,7 @@ License: MIT
Files: externals/gamemode/* Files: externals/gamemode/*
Copyright: Copyright 2017-2019 Feral Interactive Copyright: Copyright 2017-2019 Feral Interactive
License: BSD-3-Clause License: BSD-3-Clause
Files: src/android/app/debug.keystore
Copyright: 2023 yuzu Emulator Project
License: GPL-3.0-or-later

View File

@ -2,18 +2,20 @@
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
find_path(SimpleIni_INCLUDE_DIR SimpleIni.h)
include(FindPackageHandleStandardArgs) include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(SimpleIni
REQUIRED_VARS SimpleIni_INCLUDE_DIR
)
if (SimpleIni_FOUND AND NOT TARGET SimpleIni::SimpleIni) find_package(SimpleIni QUIET CONFIG)
add_library(SimpleIni::SimpleIni INTERFACE IMPORTED) if (SimpleIni_CONSIDERED_CONFIGS)
set_target_properties(SimpleIni::SimpleIni PROPERTIES find_package_handle_standard_args(SimpleIni CONFIG_MODE)
INTERFACE_INCLUDE_DIRECTORIES "${SimpleIni_INCLUDE_DIR}" else()
find_package(PkgConfig QUIET)
pkg_search_module(SIMPLEINI QUIET IMPORTED_TARGET simpleini)
find_package_handle_standard_args(SimpleIni
REQUIRED_VARS SIMPLEINI_INCLUDEDIR
VERSION_VAR SIMPLEINI_VERSION
) )
endif() endif()
mark_as_advanced(SimpleIni_INCLUDE_DIR) if (SimpleIni_FOUND AND NOT TARGET SimpleIni::SimpleIni)
add_library(SimpleIni::SimpleIni ALIAS PkgConfig::SIMPLEINI)
endif()

View File

@ -1,3 +1,14 @@
| Pull Request | Commit | Title | Author | Merged? |
|----|----|----|----|----|
| [12749](https://github.com/yuzu-emu/yuzu-android//pull/12749) | [`e3171486d`](https://github.com/yuzu-emu/yuzu-android//pull/12749/files) | general: workarounds for SMMU syncing issues | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12760](https://github.com/yuzu-emu/yuzu-android//pull/12760) | [`2c33ba278`](https://github.com/yuzu-emu/yuzu-android//pull/12760/files) | am: rewrite for multiprocess support | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12802](https://github.com/yuzu-emu/yuzu-android//pull/12802) | [`c5e88c654`](https://github.com/yuzu-emu/yuzu-android//pull/12802/files) | service: mii: Migrate service to new interface | [german77](https://github.com/german77/) | Yes |
End of merge log. You can find the original README.md below the break.
-----
<!-- <!--
SPDX-FileCopyrightText: 2018 yuzu Emulator Project SPDX-FileCopyrightText: 2018 yuzu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later SPDX-License-Identifier: GPL-2.0-or-later

View File

@ -178,6 +178,9 @@ if (NOT TARGET stb::headers)
add_library(stb::headers ALIAS stb) add_library(stb::headers ALIAS stb)
endif() endif()
add_library(tz tz/tz/tz.cpp)
target_include_directories(tz PUBLIC ./tz)
add_library(bc_decoder bc_decoder/bc_decoder.cpp) add_library(bc_decoder bc_decoder/bc_decoder.cpp)
target_include_directories(bc_decoder PUBLIC ./bc_decoder) target_include_directories(bc_decoder PUBLIC ./bc_decoder)

View File

@ -32,7 +32,7 @@ set(NX_TZDB_ARCHIVE "${CMAKE_CURRENT_BINARY_DIR}/${NX_TZDB_VERSION}.zip")
set(NX_TZDB_ROMFS_DIR "${CMAKE_CURRENT_BINARY_DIR}/nx_tzdb") set(NX_TZDB_ROMFS_DIR "${CMAKE_CURRENT_BINARY_DIR}/nx_tzdb")
if ((NOT CAN_BUILD_NX_TZDB OR YUZU_DOWNLOAD_TIME_ZONE_DATA) AND NOT EXISTS ${NX_TZDB_ARCHIVE}) if ((NOT CAN_BUILD_NX_TZDB OR YUZU_DOWNLOAD_TIME_ZONE_DATA) AND NOT EXISTS ${NX_TZDB_ROMFS_DIR})
set(NX_TZDB_DOWNLOAD_URL "https://github.com/lat9nq/tzdb_to_nx/releases/download/${NX_TZDB_VERSION}/${NX_TZDB_VERSION}.zip") set(NX_TZDB_DOWNLOAD_URL "https://github.com/lat9nq/tzdb_to_nx/releases/download/${NX_TZDB_VERSION}/${NX_TZDB_VERSION}.zip")
message(STATUS "Downloading time zone data from ${NX_TZDB_DOWNLOAD_URL}...") message(STATUS "Downloading time zone data from ${NX_TZDB_DOWNLOAD_URL}...")

View File

@ -11,6 +11,10 @@ execute_process(
WORKING_DIRECTORY ${ZONE_PATH} WORKING_DIRECTORY ${ZONE_PATH}
OUTPUT_VARIABLE FILE_LIST) OUTPUT_VARIABLE FILE_LIST)
if (NOT FILE_LIST)
message(FATAL_ERROR "No timezone files found in directory ${ZONE_PATH}, did the download fail?")
endif()
set(DIRECTORY_NAME ${HEADER_NAME}) set(DIRECTORY_NAME ${HEADER_NAME})
set(FILE_DATA "") set(FILE_DATA "")

1636
externals/tz/tz/tz.cpp vendored Normal file

File diff suppressed because it is too large Load Diff

81
externals/tz/tz/tz.h vendored Normal file
View File

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: 1996 Arthur David Olson
// SPDX-License-Identifier: BSD-2-Clause
#pragma once
#include <cstdint>
#include <limits>
#include <span>
#include <array>
#include <time.h>
namespace Tz {
using u8 = uint8_t;
using s8 = int8_t;
using u16 = uint16_t;
using s16 = int16_t;
using u32 = uint32_t;
using s32 = int32_t;
using u64 = uint64_t;
using s64 = int64_t;
constexpr size_t TZ_MAX_TIMES = 1000;
constexpr size_t TZ_MAX_TYPES = 128;
constexpr size_t TZ_MAX_CHARS = 50;
constexpr size_t MY_TZNAME_MAX = 255;
constexpr size_t TZNAME_MAXIMUM = 255;
constexpr size_t TZ_MAX_LEAPS = 50;
constexpr s64 TIME_T_MAX = std::numeric_limits<s64>::max();
constexpr s64 TIME_T_MIN = std::numeric_limits<s64>::min();
constexpr size_t CHARS_EXTRA = 3;
constexpr size_t MAX_ZONE_CHARS = std::max(TZ_MAX_CHARS + CHARS_EXTRA, sizeof("UTC"));
constexpr size_t MAX_TZNAME_CHARS = 2 * (MY_TZNAME_MAX + 1);
struct ttinfo {
s32 tt_utoff;
bool tt_isdst;
s32 tt_desigidx;
bool tt_ttisstd;
bool tt_ttisut;
};
static_assert(sizeof(ttinfo) == 0x10, "ttinfo has the wrong size!");
struct Rule {
s32 timecnt;
s32 typecnt;
s32 charcnt;
bool goback;
bool goahead;
std::array <u8, 0x2> padding0;
std::array<s64, TZ_MAX_TIMES> ats;
std::array<u8, TZ_MAX_TIMES> types;
std::array<ttinfo, TZ_MAX_TYPES> ttis;
std::array<char, std::max(MAX_ZONE_CHARS, MAX_TZNAME_CHARS)> chars;
s32 defaulttype;
std::array <u8, 0x12C4> padding1;
};
static_assert(sizeof(Rule) == 0x4000, "Rule has the wrong size!");
struct CalendarTimeInternal {
s32 tm_sec;
s32 tm_min;
s32 tm_hour;
s32 tm_mday;
s32 tm_mon;
s32 tm_year;
s32 tm_wday;
s32 tm_yday;
s32 tm_isdst;
std::array<char, 16> tm_zone;
s32 tm_utoff;
s32 time_index;
};
static_assert(sizeof(CalendarTimeInternal) == 0x3C, "CalendarTimeInternal has the wrong size!");
s32 ParseTimeZoneBinary(Rule& out_rule, std::span<const u8> binary);
bool localtime_rz(CalendarTimeInternal* tmp, Rule* sp, time_t* timep);
u32 mktime_tzname(time_t* out_time, Rule* sp, CalendarTimeInternal* tmp);
} // namespace Tz

View File

@ -82,8 +82,8 @@ android {
} }
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE") val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
if (keystoreFile != null) { signingConfigs {
signingConfigs { if (keystoreFile != null) {
create("release") { create("release") {
storeFile = file(keystoreFile) storeFile = file(keystoreFile)
storePassword = System.getenv("ANDROID_KEYSTORE_PASS") storePassword = System.getenv("ANDROID_KEYSTORE_PASS")
@ -91,6 +91,12 @@ android {
keyPassword = System.getenv("ANDROID_KEYSTORE_PASS") keyPassword = System.getenv("ANDROID_KEYSTORE_PASS")
} }
} }
create("default") {
storeFile = file("$projectDir/debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
} }
// Define build types, which are orthogonal to product flavors. // Define build types, which are orthogonal to product flavors.
@ -101,7 +107,7 @@ android {
signingConfig = if (keystoreFile != null) { signingConfig = if (keystoreFile != null) {
signingConfigs.getByName("release") signingConfigs.getByName("release")
} else { } else {
signingConfigs.getByName("debug") signingConfigs.getByName("default")
} }
resValue("string", "app_name_suffixed", "yuzu") resValue("string", "app_name_suffixed", "yuzu")
@ -118,7 +124,7 @@ android {
register("relWithDebInfo") { register("relWithDebInfo") {
isDefault = true isDefault = true
resValue("string", "app_name_suffixed", "yuzu Debug Release") resValue("string", "app_name_suffixed", "yuzu Debug Release")
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("default")
isMinifyEnabled = true isMinifyEnabled = true
isDebuggable = true isDebuggable = true
proguardFiles( proguardFiles(
@ -133,6 +139,7 @@ android {
// Signed by debug key disallowing distribution on Play Store. // Signed by debug key disallowing distribution on Play Store.
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
debug { debug {
signingConfig = signingConfigs.getByName("default")
resValue("string", "app_name_suffixed", "yuzu Debug") resValue("string", "app_name_suffixed", "yuzu Debug")
isDebuggable = true isDebuggable = true
isJniDebuggable = true isJniDebuggable = true
@ -188,8 +195,15 @@ tasks.create<Delete>("ktlintReset") {
delete(File(buildDir.path + File.separator + "intermediates/ktLint")) delete(File(buildDir.path + File.separator + "intermediates/ktLint"))
} }
val showFormatHelp = {
logger.lifecycle(
"If this check fails, please try running \"gradlew ktlintFormat\" for automatic " +
"codestyle fixes"
)
}
tasks.getByPath("ktlintKotlinScriptCheck").doFirst { showFormatHelp.invoke() }
tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset") tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
tasks.getByPath("preBuild").dependsOn("ktlintCheck")
ktlint { ktlint {
version.set("0.47.1") version.set("0.47.1")
@ -228,71 +242,33 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
} }
fun getGitVersion(): String { fun runGitCommand(command: List<String>): String {
var versionName = "0.0" return try {
ProcessBuilder(command)
try {
versionName = ProcessBuilder("git", "describe", "--always", "--long")
.directory(project.rootDir) .directory(project.rootDir)
.redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE)
.start().inputStream.bufferedReader().use { it.readText() } .start().inputStream.bufferedReader().use { it.readText() }
.trim() .trim()
} catch (e: Exception) {
logger.error("Cannot find git")
""
}
}
fun getGitVersion(): String {
val versionName = if (System.getenv("GITHUB_ACTIONS") != null) {
val gitTag = System.getenv("GIT_TAG_NAME") ?: ""
gitTag
} else {
runGitCommand(listOf("git", "describe", "--always", "--long"))
.replace(Regex("(-0)?-[^-]+$"), "") .replace(Regex("(-0)?-[^-]+$"), "")
} catch (e: Exception) {
logger.error("Cannot find git, defaulting to dummy version number")
} }
return versionName.ifEmpty { "0.0" }
if (System.getenv("GITHUB_ACTIONS") != null) {
val gitTag = System.getenv("GIT_TAG_NAME")
versionName = gitTag ?: versionName
}
return versionName
} }
fun getGitHash(): String { fun getGitHash(): String =
try { runGitCommand(listOf("git", "rev-parse", "--short", "HEAD")).ifEmpty { "dummy-hash" }
val processBuilder = ProcessBuilder("git", "rev-parse", "--short", "HEAD")
processBuilder.directory(project.rootDir)
val process = processBuilder.start()
val inputStream = process.inputStream
val errorStream = process.errorStream
process.waitFor()
return if (process.exitValue() == 0) { fun getBranch(): String =
inputStream.bufferedReader() runGitCommand(listOf("git", "rev-parse", "--abbrev-ref", "HEAD")).ifEmpty { "dummy-hash" }
.use { it.readText().trim() } // return the value of gitHash
} else {
val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
logger.error("Error running git command: $errorMessage")
"dummy-hash" // return a dummy hash value in case of an error
}
} catch (e: Exception) {
logger.error("$e: Cannot find git, defaulting to dummy build hash")
return "dummy-hash" // return a dummy hash value in case of an error
}
}
fun getBranch(): String {
try {
val processBuilder = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")
processBuilder.directory(project.rootDir)
val process = processBuilder.start()
val inputStream = process.inputStream
val errorStream = process.errorStream
process.waitFor()
return if (process.exitValue() == 0) {
inputStream.bufferedReader()
.use { it.readText().trim() } // return the value of gitHash
} else {
val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
logger.error("Error running git command: $errorMessage")
"dummy-hash" // return a dummy hash value in case of an error
}
} catch (e: Exception) {
logger.error("$e: Cannot find git, defaulting to dummy build hash")
return "dummy-hash" // return a dummy hash value in case of an error
}
}

Binary file not shown.

View File

@ -21,6 +21,9 @@ import org.yuzu.yuzu_emu.utils.DocumentsTree
import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
import org.yuzu.yuzu_emu.model.InstallResult
import org.yuzu.yuzu_emu.model.Patch
import org.yuzu.yuzu_emu.model.GameVerificationResult
/** /**
* Class which contains methods that interact * Class which contains methods that interact
@ -235,9 +238,12 @@ object NativeLibrary {
/** /**
* Installs a nsp or xci file to nand * Installs a nsp or xci file to nand
* @param filename String representation of file uri * @param filename String representation of file uri
* @param extension Lowercase string representation of file extension without "." * @return int representation of [InstallResult]
*/ */
external fun installFileToNand(filename: String, extension: String): Int external fun installFileToNand(
filename: String,
callback: (max: Long, progress: Long) -> Boolean
): Int
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
@ -297,6 +303,11 @@ object NativeLibrary {
*/ */
external fun getCpuBackend(): String external fun getCpuBackend(): String
/**
* Returns the current GPU Driver.
*/
external fun getGpuDriver(): String
external fun applySettings() external fun applySettings()
external fun logSettings() external fun logSettings()
@ -535,9 +546,49 @@ object NativeLibrary {
* *
* @param path Path to game file. Can be a [Uri]. * @param path Path to game file. Can be a [Uri].
* @param programId String representation of a game's program ID * @param programId String representation of a game's program ID
* @return Array of pairs where the first value is the name of an addon and the second is the version * @return Array of available patches
*/ */
external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>? external fun getPatchesForFile(path: String, programId: String): Array<Patch>?
/**
* Removes an update for a given [programId]
* @param programId String representation of a game's program ID
*/
external fun removeUpdate(programId: String)
/**
* Removes all DLC for a [programId]
* @param programId String representation of a game's program ID
*/
external fun removeDLC(programId: String)
/**
* Removes a mod installed for a given [programId]
* @param programId String representation of a game's program ID
* @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name
* of the mod's directory in a game's load folder.
*/
external fun removeMod(programId: String, name: String)
/**
* Verifies all installed content
* @param callback UI callback for verification progress. Return true in the callback to cancel.
* @return Array of content that failed verification. Successful if empty.
*/
external fun verifyInstalledContents(
callback: (max: Long, progress: Long) -> Boolean
): Array<String>
/**
* Verifies the contents of a game
* @param path String path to a game
* @param callback UI callback for verification progress. Return true in the callback to cancel.
* @return Int that is meant to be converted to a [GameVerificationResult]
*/
external fun verifyGameContents(
path: String,
callback: (max: Long, progress: Long) -> Boolean
): Int
/** /**
* Gets the save location for a specific game * Gets the save location for a specific game
@ -568,6 +619,11 @@ object NativeLibrary {
*/ */
external fun clearFilesystemProvider() external fun clearFilesystemProvider()
/**
* Checks if all necessary keys are present for decryption
*/
external fun areKeysPresent(): Boolean
/** /**
* Button type for use in onTouchEvent * Button type for use in onTouchEvent
*/ */
@ -609,15 +665,4 @@ object NativeLibrary {
const val RELEASED = 0 const val RELEASED = 0
const val PRESSED = 1 const val PRESSED = 1
} }
/**
* Result from installFileToNand
*/
object InstallFileToNandResult {
const val Success = 0
const val SuccessFileOverwritten = 1
const val Error = 2
const val ErrorBaseGame = 3
const val ErrorFilenameExtension = 4
}
} }

View File

@ -49,7 +49,6 @@ 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
@ -171,11 +170,6 @@ 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.getBoolean() && !isInPictureInPictureMode) { if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
@ -199,6 +193,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }
if (emulationViewModel.drawerOpen.value) {
return super.dispatchKeyEvent(event)
}
return InputHandler.dispatchKeyEvent(event) return InputHandler.dispatchKeyEvent(event)
} }
@ -209,6 +207,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
return super.dispatchGenericMotionEvent(event) return super.dispatchGenericMotionEvent(event)
} }
if (emulationViewModel.drawerOpen.value) {
return super.dispatchGenericMotionEvent(event)
}
// Don't attempt to do anything if we are disconnecting a device. // Don't attempt to do anything if we are disconnecting a device.
if (event.actionMasked == MotionEvent.ACTION_CANCEL) { if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
return true return true

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.adapters
import android.annotation.SuppressLint
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
import androidx.recyclerview.widget.RecyclerView
/**
* Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate
* code used in every [RecyclerView].
* Type assigned to [Model] must inherit from [Object] in order to be compared properly.
* @param exact Decides whether each item will be compared by reference or by their contents
*/
abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
exact: Boolean = true
) : ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>(exact)).build()) {
override fun onBindViewHolder(holder: Holder, position: Int) =
holder.bind(currentList[position])
private class DiffCallback<Model>(val exact: Boolean) : DiffUtil.ItemCallback<Model>() {
override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
if (exact) {
return oldItem === newItem
}
return oldItem == newItem
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
return oldItem == newItem
}
}
}

View File

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.adapters
import android.annotation.SuppressLint
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
/**
* Generic list class meant to take care of basic lists
* @param currentList The list to show initially
*/
abstract class AbstractListAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
open var currentList: List<Model>
) : RecyclerView.Adapter<Holder>() {
override fun onBindViewHolder(holder: Holder, position: Int) =
holder.bind(currentList[position])
override fun getItemCount(): Int = currentList.size
/**
* Adds an item to [currentList] and notifies the underlying adapter of the change. If no parameter
* is passed in for position, [item] is added to the end of the list. Invokes [callback] last.
* @param item The item to add to the list
* @param position Index where [item] will be added
* @param callback Lambda that's called at the end of the list changes and has the added list
* position passed in as a parameter
*/
open fun addItem(item: Model, position: Int = -1, callback: ((position: Int) -> Unit)? = null) {
val newList = currentList.toMutableList()
val positionToUpdate: Int
if (position == -1) {
newList.add(item)
currentList = newList
positionToUpdate = currentList.size - 1
} else {
newList.add(position, item)
currentList = newList
positionToUpdate = position
}
onItemAdded(positionToUpdate, callback)
}
protected fun onItemAdded(position: Int, callback: ((Int) -> Unit)? = null) {
notifyItemInserted(position)
callback?.invoke(position)
}
/**
* Replaces the [item] at [position] in the [currentList] and notifies the underlying adapter
* of the change. Invokes [callback] last.
* @param item New list item
* @param position Index where [item] will replace the existing list item
* @param callback Lambda that's called at the end of the list changes and has the changed list
* position passed in as a parameter
*/
fun changeItem(item: Model, position: Int, callback: ((position: Int) -> Unit)? = null) {
val newList = currentList.toMutableList()
newList[position] = item
currentList = newList
onItemChanged(position, callback)
}
protected fun onItemChanged(position: Int, callback: ((Int) -> Unit)? = null) {
notifyItemChanged(position)
callback?.invoke(position)
}
/**
* Removes the list item at [position] in [currentList] and notifies the underlying adapter
* of the change. Invokes [callback] last.
* @param position Index where the list item will be removed
* @param callback Lambda that's called at the end of the list changes and has the removed list
* position passed in as a parameter
*/
fun removeItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
val newList = currentList.toMutableList()
newList.removeAt(position)
currentList = newList
onItemRemoved(position, callback)
}
protected fun onItemRemoved(position: Int, callback: ((Int) -> Unit)? = null) {
notifyItemRemoved(position)
callback?.invoke(position)
}
/**
* Replaces [currentList] with [newList] and notifies the underlying adapter of the change.
* @param newList The new list to replace [currentList]
*/
@SuppressLint("NotifyDataSetChanged")
open fun replaceList(newList: List<Model>) {
currentList = newList
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.adapters
import org.yuzu.yuzu_emu.model.SelectableItem
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
/**
* Generic list class meant to take care of single selection UI updates
* @param currentList The list to show initially
* @param defaultSelection The default selection to use if no list items are selected by
* [SelectableItem.selected] or if the currently selected item is removed from the list
*/
abstract class AbstractSingleSelectionList<
Model : SelectableItem,
Holder : AbstractViewHolder<Model>
>(
final override var currentList: List<Model>,
private val defaultSelection: DefaultSelection = DefaultSelection.Start
) : AbstractListAdapter<Model, Holder>(currentList) {
var selectedItem = getDefaultSelection()
init {
findSelectedItem()
}
/**
* Changes the selection state of the [SelectableItem] that was selected and the previously selected
* item and notifies the underlying adapter of the change for those items. Invokes [callback] last.
* Does nothing if [position] is the same as the currently selected item.
* @param position Index of the item that was selected
* @param callback Lambda that's called at the end of the list changes and has the selected list
* position passed in as a parameter
*/
fun selectItem(position: Int, callback: ((position: Int) -> Unit)? = null) {
if (position == selectedItem) {
return
}
val previouslySelectedItem = selectedItem
selectedItem = position
if (currentList.indices.contains(selectedItem)) {
currentList[selectedItem].onSelectionStateChanged(true)
}
if (currentList.indices.contains(previouslySelectedItem)) {
currentList[previouslySelectedItem].onSelectionStateChanged(false)
}
onItemChanged(previouslySelectedItem)
onItemChanged(selectedItem)
callback?.invoke(position)
}
/**
* Removes a given item from the list and notifies the underlying adapter of the change. If the
* currently selected item was the item that was removed, the item at the position provided
* by [defaultSelection] will be made the new selection. Invokes [callback] last.
* @param position Index of the item that was removed
* @param callback Lambda that's called at the end of the list changes and has the removed and
* selected list positions passed in as parameters
*/
fun removeSelectableItem(
position: Int,
callback: ((removedPosition: Int, selectedPosition: Int) -> Unit)?
) {
removeItem(position)
if (position == selectedItem) {
selectedItem = getDefaultSelection()
currentList[selectedItem].onSelectionStateChanged(true)
onItemChanged(selectedItem)
} else if (position < selectedItem) {
selectedItem--
}
callback?.invoke(position, selectedItem)
}
override fun addItem(item: Model, position: Int, callback: ((Int) -> Unit)?) {
super.addItem(item, position, callback)
if (position <= selectedItem && position != -1) {
selectedItem++
}
}
override fun replaceList(newList: List<Model>) {
super.replaceList(newList)
findSelectedItem()
}
private fun findSelectedItem() {
for (i in currentList.indices) {
if (currentList[i].selected) {
selectedItem = i
break
}
}
}
private fun getDefaultSelection(): Int =
when (defaultSelection) {
DefaultSelection.Start -> currentList.indices.first
DefaultSelection.End -> currentList.indices.last
}
enum class DefaultSelection { Start, End }
}

View File

@ -5,48 +5,33 @@ package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup 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.databinding.ListItemAddonBinding
import org.yuzu.yuzu_emu.model.Addon import org.yuzu.yuzu_emu.model.Patch
import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>( class AddonAdapter(val addonViewModel: AddonViewModel) :
AsyncDifferConfig.Builder(DiffCallback()).build() AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() {
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return AddonViewHolder(it) } .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) : inner class AddonViewHolder(val binding: ListItemAddonBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<Patch>(binding) {
fun bind(addon: Addon) { override fun bind(model: Patch) {
binding.root.setOnClickListener { binding.root.setOnClickListener {
binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked
} }
binding.title.text = addon.title binding.title.text = model.name
binding.version.text = addon.version binding.version.text = model.version
binding.addonSwitch.setOnCheckedChangeListener { _, checked -> binding.addonCheckbox.setOnCheckedChangeListener { _, checked ->
addon.enabled = checked model.enabled = checked
}
binding.addonCheckbox.isChecked = model.enabled
binding.buttonDelete.setOnClickListener {
addonViewModel.setAddonToDelete(model)
} }
binding.addonSwitch.isChecked = addon.enabled
}
}
private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
return oldItem == newItem
} }
} }
} }

View File

@ -4,13 +4,11 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.recyclerview.widget.RecyclerView
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
@ -19,72 +17,58 @@ 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
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) : class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) :
RecyclerView.Adapter<AppletAdapter.AppletViewHolder>(), AbstractListAdapter<Applet, AppletAdapter.AppletViewHolder>(applets) {
View.OnClickListener {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): AppletAdapter.AppletViewHolder { ): AppletAdapter.AppletViewHolder {
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false) CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.apply { root.setOnClickListener(this@AppletAdapter) }
.also { return AppletViewHolder(it) } .also { return AppletViewHolder(it) }
} }
override fun onBindViewHolder(holder: AppletViewHolder, position: Int) =
holder.bind(applets[position])
override fun getItemCount(): Int = applets.size
override fun onClick(view: View) {
val applet = (view.tag as AppletViewHolder).applet
val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
if (appletPath.isEmpty()) {
Toast.makeText(
YuzuApplication.appContext,
R.string.applets_error_applet,
Toast.LENGTH_SHORT
).show()
return
}
if (applet.appletInfo == AppletInfo.Cabinet) {
view.findNavController()
.navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
return
}
NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
val appletGame = Game(
title = YuzuApplication.appContext.getString(applet.titleId),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
view.findNavController().navigate(action)
}
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) : inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<Applet>(binding) {
lateinit var applet: Applet override fun bind(model: Applet) {
binding.title.setText(model.titleId)
init { binding.description.setText(model.descriptionId)
itemView.tag = this
}
fun bind(applet: Applet) {
this.applet = applet
binding.title.setText(applet.titleId)
binding.description.setText(applet.descriptionId)
binding.icon.setImageDrawable( binding.icon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
binding.icon.context.resources, binding.icon.context.resources,
applet.iconId, model.iconId,
binding.icon.context.theme binding.icon.context.theme
) )
) )
binding.root.setOnClickListener { onClick(model) }
}
fun onClick(applet: Applet) {
val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
if (appletPath.isEmpty()) {
Toast.makeText(
binding.root.context,
R.string.applets_error_applet,
Toast.LENGTH_SHORT
).show()
return
}
if (applet.appletInfo == AppletInfo.Cabinet) {
binding.root.findNavController()
.navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
return
}
NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
val appletGame = Game(
title = YuzuApplication.appContext.getString(applet.titleId),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
binding.root.findNavController().navigate(action)
} }
} }
} }

View File

@ -4,12 +4,10 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
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
@ -19,54 +17,43 @@ import org.yuzu.yuzu_emu.model.CabinetMode
import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder import org.yuzu.yuzu_emu.adapters.CabinetLauncherDialogAdapter.CabinetModeViewHolder
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
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class CabinetLauncherDialogAdapter(val fragment: Fragment) : class CabinetLauncherDialogAdapter(val fragment: Fragment) :
RecyclerView.Adapter<CabinetModeViewHolder>(), AbstractListAdapter<CabinetMode, CabinetModeViewHolder>(
View.OnClickListener { CabinetMode.values().copyOfRange(1, CabinetMode.entries.size).toList()
private val cabinetModes = CabinetMode.values().copyOfRange(1, CabinetMode.values().size) ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CabinetModeViewHolder {
DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) DialogListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.apply { root.setOnClickListener(this@CabinetLauncherDialogAdapter) }
.also { return CabinetModeViewHolder(it) } .also { return CabinetModeViewHolder(it) }
} }
override fun getItemCount(): Int = cabinetModes.size
override fun onBindViewHolder(holder: CabinetModeViewHolder, position: Int) =
holder.bind(cabinetModes[position])
override fun onClick(view: View) {
val mode = (view.tag as CabinetModeViewHolder).cabinetMode
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
NativeLibrary.setCabinetMode(mode.id)
val appletGame = Game(
title = YuzuApplication.appContext.getString(R.string.cabinet_applet),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
fragment.findNavController().navigate(action)
}
inner class CabinetModeViewHolder(val binding: DialogListItemBinding) : inner class CabinetModeViewHolder(val binding: DialogListItemBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<CabinetMode>(binding) {
lateinit var cabinetMode: CabinetMode override fun bind(model: CabinetMode) {
init {
itemView.tag = this
}
fun bind(cabinetMode: CabinetMode) {
this.cabinetMode = cabinetMode
binding.icon.setImageDrawable( binding.icon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
binding.icon.context.resources, binding.icon.context.resources,
cabinetMode.iconId, model.iconId,
binding.icon.context.theme binding.icon.context.theme
) )
) )
binding.title.setText(cabinetMode.titleId) binding.title.setText(model.titleId)
binding.root.setOnClickListener { onClick(model) }
}
private fun onClick(mode: CabinetMode) {
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.Cabinet.entryId)
NativeLibrary.setCurrentAppletId(AppletInfo.Cabinet.appletId)
NativeLibrary.setCabinetMode(mode.id)
val appletGame = Game(
title = YuzuApplication.appContext.getString(R.string.cabinet_applet),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
fragment.findNavController().navigate(action)
} }
} }
} }

View File

@ -7,65 +7,39 @@ import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.model.Driver
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
class DriverAdapter(private val driverViewModel: DriverViewModel) : class DriverAdapter(private val driverViewModel: DriverViewModel) :
ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>( AbstractSingleSelectionList<Driver, DriverAdapter.DriverViewHolder>(
AsyncDifferConfig.Builder(DiffCallback()).build() driverViewModel.driverList.value
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
val binding = CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) .also { return DriverViewHolder(it) }
return DriverViewHolder(binding)
}
override fun getItemCount(): Int = currentList.size
override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
holder.bind(currentList[position])
private fun onSelectDriver(position: Int) {
driverViewModel.setSelectedDriverIndex(position)
notifyItemChanged(driverViewModel.previouslySelectedDriver)
notifyItemChanged(driverViewModel.selectedDriver)
}
private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) {
if (driverViewModel.selectedDriver > position) {
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
}
if (GpuDriverHelper.customDriverSettingData == driverData.second) {
driverViewModel.setSelectedDriverIndex(0)
}
driverViewModel.driversToDelete.add(driverData.first)
driverViewModel.removeDriver(driverData)
notifyItemRemoved(position)
notifyItemChanged(driverViewModel.selectedDriver)
} }
inner class DriverViewHolder(val binding: CardDriverOptionBinding) : inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<Driver>(binding) {
private lateinit var driverData: Pair<String, GpuDriverMetadata> override fun bind(model: Driver) {
fun bind(driverData: Pair<String, GpuDriverMetadata>) {
this.driverData = driverData
val driver = driverData.second
binding.apply { binding.apply {
radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition radioButton.isChecked = model.selected
root.setOnClickListener { root.setOnClickListener {
onSelectDriver(bindingAdapterPosition) selectItem(bindingAdapterPosition) {
driverViewModel.onDriverSelected(it)
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
}
} }
buttonDelete.setOnClickListener { buttonDelete.setOnClickListener {
onDeleteDriver(driverData, bindingAdapterPosition) removeSelectableItem(
bindingAdapterPosition
) { removedPosition: Int, selectedPosition: Int ->
driverViewModel.onDriverRemoved(removedPosition, selectedPosition)
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
}
} }
// Delay marquee by 3s // Delay marquee by 3s
@ -80,38 +54,19 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
}, },
3000 3000
) )
if (driver.name == null) { title.text = model.title
title.setText(R.string.system_gpu_driver) version.text = model.version
description.text = "" description.text = model.description
version.text = "" if (model.description.isNotEmpty()) {
version.visibility = View.GONE
description.visibility = View.GONE
buttonDelete.visibility = View.GONE
} else {
title.text = driver.name
version.text = driver.version
description.text = driver.description
version.visibility = View.VISIBLE version.visibility = View.VISIBLE
description.visibility = View.VISIBLE description.visibility = View.VISIBLE
buttonDelete.visibility = View.VISIBLE buttonDelete.visibility = View.VISIBLE
} else {
version.visibility = View.GONE
description.visibility = View.GONE
buttonDelete.visibility = View.GONE
} }
} }
} }
} }
private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() {
override fun areItemsTheSame(
oldItem: Pair<String, GpuDriverMetadata>,
newItem: Pair<String, GpuDriverMetadata>
): Boolean {
return oldItem.first == newItem.first
}
override fun areContentsTheSame(
oldItem: Pair<String, GpuDriverMetadata>,
newItem: Pair<String, GpuDriverMetadata>
): Boolean {
return oldItem.second == newItem.second
}
}
} }

View File

@ -8,19 +8,14 @@ import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.databinding.CardFolderBinding import org.yuzu.yuzu_emu.databinding.CardFolderBinding
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
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.viewholder.AbstractViewHolder
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
ListAdapter<GameDir, FolderAdapter.FolderViewHolder>( AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() {
AsyncDifferConfig.Builder(DiffCallback()).build()
) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
@ -29,18 +24,11 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
.also { return FolderViewHolder(it) } .also { return FolderViewHolder(it) }
} }
override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
holder.bind(currentList[position])
inner class FolderViewHolder(val binding: CardFolderBinding) : inner class FolderViewHolder(val binding: CardFolderBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<GameDir>(binding) {
private lateinit var gameDir: GameDir override fun bind(model: GameDir) {
fun bind(gameDir: GameDir) {
this.gameDir = gameDir
binding.apply { binding.apply {
path.text = Uri.parse(gameDir.uriString).path path.text = Uri.parse(model.uriString).path
path.postDelayed( path.postDelayed(
{ {
path.isSelected = true path.isSelected = true
@ -50,7 +38,7 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
) )
buttonEdit.setOnClickListener { buttonEdit.setOnClickListener {
GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir) GameFolderPropertiesDialogFragment.newInstance(model)
.show( .show(
activity.supportFragmentManager, activity.supportFragmentManager,
GameFolderPropertiesDialogFragment.TAG GameFolderPropertiesDialogFragment.TAG
@ -58,19 +46,9 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
} }
buttonDelete.setOnClickListener { buttonDelete.setOnClickListener {
gamesViewModel.removeFolder(this@FolderViewHolder.gameDir) gamesViewModel.removeFolder(model)
} }
} }
} }
} }
private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
return oldItem == newItem
}
}
} }

View File

@ -3,155 +3,46 @@
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.LayerDrawable
import android.net.Uri import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.drawable.toDrawable
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.HomeNavigationDirections
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.activities.EmulationActivity
import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.databinding.CardGameBinding
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class GameAdapter(private val activity: AppCompatActivity) : class GameAdapter(private val activity: AppCompatActivity) :
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
View.OnClickListener,
View.OnLongClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
// Create a new view. CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) .also { return GameViewHolder(it) }
binding.cardGame.setOnClickListener(this)
binding.cardGame.setOnLongClickListener(this)
// Use that view to create a ViewHolder.
return GameViewHolder(binding)
}
override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
holder.bind(currentList[position])
override fun getItemCount(): Int = currentList.size
/**
* Launches the game that was clicked on.
*
* @param view The card representing the game the user wants to play.
*/
override fun onClick(view: View) {
val holder = view.tag as GameViewHolder
val gameExists = DocumentFile.fromSingleUri(
YuzuApplication.appContext,
Uri.parse(holder.game.path)
)?.exists() == true
if (!gameExists) {
Toast.makeText(
YuzuApplication.appContext,
R.string.loader_error_file_not_found,
Toast.LENGTH_LONG
).show()
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
return
}
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
preferences.edit()
.putLong(
holder.game.keyLastPlayedTime,
System.currentTimeMillis()
)
.apply()
val openIntent = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = Uri.parse(holder.game.path)
}
activity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val layerDrawable = ResourcesCompat.getDrawable(
YuzuApplication.appContext.resources,
R.drawable.shortcut,
null
) as LayerDrawable
layerDrawable.setDrawableByLayerId(
R.id.shortcut_foreground,
GameIconUtils.getGameIcon(activity, holder.game)
.toDrawable(YuzuApplication.appContext.resources)
)
val inset = YuzuApplication.appContext.resources
.getDimensionPixelSize(R.dimen.icon_inset)
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
val shortcut =
ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path)
.setShortLabel(holder.game.title)
.setIcon(
IconCompat.createWithAdaptiveBitmap(
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
)
)
.setIntent(openIntent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
}
}
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
view.findNavController().navigate(action)
}
override fun onLongClick(view: View): Boolean {
val holder = view.tag as GameViewHolder
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
view.findNavController().navigate(action)
return true
} }
inner class GameViewHolder(val binding: CardGameBinding) : inner class GameViewHolder(val binding: CardGameBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<Game>(binding) {
lateinit var game: Game override fun bind(model: Game) {
init {
binding.cardGame.tag = this
}
fun bind(game: Game) {
this.game = game
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
GameIconUtils.loadGameIcon(game, binding.imageGameScreen) GameIconUtils.loadGameIcon(model, binding.imageGameScreen)
binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
binding.textGameTitle.postDelayed( binding.textGameTitle.postDelayed(
{ {
@ -160,16 +51,56 @@ class GameAdapter(private val activity: AppCompatActivity) :
}, },
3000 3000
) )
}
}
private class DiffCallback : DiffUtil.ItemCallback<Game>() { binding.cardGame.setOnClickListener { onClick(model) }
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { binding.cardGame.setOnLongClickListener { onLongClick(model) }
return oldItem == newItem
} }
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { fun onClick(game: Game) {
return oldItem == newItem val gameExists = DocumentFile.fromSingleUri(
YuzuApplication.appContext,
Uri.parse(game.path)
)?.exists() == true
if (!gameExists) {
Toast.makeText(
YuzuApplication.appContext,
R.string.loader_error_file_not_found,
Toast.LENGTH_LONG
).show()
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
return
}
val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
preferences.edit()
.putLong(
game.keyLastPlayedTime,
System.currentTimeMillis()
)
.apply()
activity.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val shortcut =
ShortcutInfoCompat.Builder(YuzuApplication.appContext, game.path)
.setShortLabel(game.title)
.setIcon(GameIconUtils.getShortcutIcon(activity, game))
.setIntent(game.launchIntent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut)
}
}
val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true)
binding.root.findNavController().navigate(action)
}
fun onLongClick(game: Game): Boolean {
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
binding.root.findNavController().navigate(action)
return true
} }
} }
} }

View File

@ -12,23 +12,22 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
import org.yuzu.yuzu_emu.model.GameProperty import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.InstallableProperty import org.yuzu.yuzu_emu.model.InstallableProperty
import org.yuzu.yuzu_emu.model.SubmenuProperty import org.yuzu.yuzu_emu.model.SubmenuProperty
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class GamePropertiesAdapter( class GamePropertiesAdapter(
private val viewLifecycle: LifecycleOwner, private val viewLifecycle: LifecycleOwner,
private var properties: List<GameProperty> private var properties: List<GameProperty>
) : ) : AbstractListAdapter<GameProperty, AbstractViewHolder<GameProperty>>(properties) {
RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): GamePropertyViewHolder { ): AbstractViewHolder<GameProperty> {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
PropertyType.Submenu.ordinal -> { PropertyType.Submenu.ordinal -> {
@ -51,11 +50,6 @@ class GamePropertiesAdapter(
} }
} }
override fun getItemCount(): Int = properties.size
override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) =
holder.bind(properties[position])
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when (properties[position]) { return when (properties[position]) {
is SubmenuProperty -> PropertyType.Submenu.ordinal is SubmenuProperty -> PropertyType.Submenu.ordinal
@ -63,14 +57,10 @@ class GamePropertiesAdapter(
} }
} }
sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(property: GameProperty)
}
inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) : inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
GamePropertyViewHolder(binding.root) { AbstractViewHolder<GameProperty>(binding) {
override fun bind(property: GameProperty) { override fun bind(model: GameProperty) {
val submenuProperty = property as SubmenuProperty val submenuProperty = model as SubmenuProperty
binding.root.setOnClickListener { binding.root.setOnClickListener {
submenuProperty.action.invoke() submenuProperty.action.invoke()
@ -108,9 +98,9 @@ class GamePropertiesAdapter(
} }
inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) : inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
GamePropertyViewHolder(binding.root) { AbstractViewHolder<GameProperty>(binding) {
override fun bind(property: GameProperty) { override fun bind(model: GameProperty) {
val installableProperty = property as InstallableProperty val installableProperty = model as InstallableProperty
binding.title.setText(installableProperty.titleId) binding.title.setText(installableProperty.titleId)
binding.description.setText(installableProperty.descriptionId) binding.description.setText(installableProperty.descriptionId)

View File

@ -14,69 +14,37 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
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.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class HomeSettingAdapter( class HomeSettingAdapter(
private val activity: AppCompatActivity, private val activity: AppCompatActivity,
private val viewLifecycle: LifecycleOwner, private val viewLifecycle: LifecycleOwner,
var options: List<HomeSetting> options: List<HomeSetting>
) : ) : AbstractListAdapter<HomeSetting, HomeSettingAdapter.HomeOptionViewHolder>(options) {
RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(),
View.OnClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
val binding = CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) .also { return HomeOptionViewHolder(it) }
binding.root.setOnClickListener(this)
return HomeOptionViewHolder(binding)
}
override fun getItemCount(): Int {
return options.size
}
override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
holder.bind(options[position])
}
override fun onClick(view: View) {
val holder = view.tag as HomeOptionViewHolder
if (holder.option.isEnabled.invoke()) {
holder.option.onClick.invoke()
} else {
MessageDialogFragment.newInstance(
activity,
titleId = holder.option.disabledTitleId,
descriptionId = holder.option.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
}
} }
inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) : inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<HomeSetting>(binding) {
lateinit var option: HomeSetting override fun bind(model: HomeSetting) {
binding.optionTitle.text = activity.resources.getString(model.titleId)
init { binding.optionDescription.text = activity.resources.getString(model.descriptionId)
itemView.tag = this
}
fun bind(option: HomeSetting) {
this.option = option
binding.optionTitle.text = activity.resources.getString(option.titleId)
binding.optionDescription.text = activity.resources.getString(option.descriptionId)
binding.optionIcon.setImageDrawable( binding.optionIcon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
activity.resources, activity.resources,
option.iconId, model.iconId,
activity.theme activity.theme
) )
) )
when (option.titleId) { when (model.titleId) {
R.string.get_early_access -> R.string.get_early_access ->
binding.optionLayout.background = binding.optionLayout.background =
ContextCompat.getDrawable( ContextCompat.getDrawable(
@ -85,7 +53,7 @@ class HomeSettingAdapter(
) )
} }
if (!option.isEnabled.invoke()) { if (!model.isEnabled.invoke()) {
binding.optionTitle.alpha = 0.5f binding.optionTitle.alpha = 0.5f
binding.optionDescription.alpha = 0.5f binding.optionDescription.alpha = 0.5f
binding.optionIcon.alpha = 0.5f binding.optionIcon.alpha = 0.5f
@ -93,7 +61,7 @@ class HomeSettingAdapter(
viewLifecycle.lifecycleScope.launch { viewLifecycle.lifecycleScope.launch {
viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
option.details.collect { updateOptionDetails(it) } model.details.collect { updateOptionDetails(it) }
} }
} }
binding.optionDetail.postDelayed( binding.optionDetail.postDelayed(
@ -103,6 +71,20 @@ class HomeSettingAdapter(
}, },
3000 3000
) )
binding.root.setOnClickListener { onClick(model) }
}
private fun onClick(model: HomeSetting) {
if (model.isEnabled.invoke()) {
model.onClick.invoke()
} else {
MessageDialogFragment.newInstance(
activity,
titleId = model.disabledTitleId,
descriptionId = model.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
}
} }
private fun updateOptionDetails(detailString: String) { private fun updateOptionDetails(detailString: String) {

View File

@ -6,43 +6,33 @@ package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.databinding.CardInstallableBinding import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
import org.yuzu.yuzu_emu.model.Installable import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class InstallableAdapter(private val installables: List<Installable>) : class InstallableAdapter(installables: List<Installable>) :
RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { AbstractListAdapter<Installable, InstallableAdapter.InstallableViewHolder>(installables) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): InstallableAdapter.InstallableViewHolder { ): InstallableAdapter.InstallableViewHolder {
val binding = CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) .also { return InstallableViewHolder(it) }
return InstallableViewHolder(binding)
} }
override fun getItemCount(): Int = installables.size
override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) =
holder.bind(installables[position])
inner class InstallableViewHolder(val binding: CardInstallableBinding) : inner class InstallableViewHolder(val binding: CardInstallableBinding) :
RecyclerView.ViewHolder(binding.root) { AbstractViewHolder<Installable>(binding) {
lateinit var installable: Installable override fun bind(model: Installable) {
binding.title.setText(model.titleId)
binding.description.setText(model.descriptionId)
fun bind(installable: Installable) { if (model.install != null) {
this.installable = installable
binding.title.setText(installable.titleId)
binding.description.setText(installable.descriptionId)
if (installable.install != null) {
binding.buttonInstall.visibility = View.VISIBLE binding.buttonInstall.visibility = View.VISIBLE
binding.buttonInstall.setOnClickListener { installable.install.invoke() } binding.buttonInstall.setOnClickListener { model.install.invoke() }
} }
if (installable.export != null) { if (model.export != null) {
binding.buttonExport.visibility = View.VISIBLE binding.buttonExport.visibility = View.VISIBLE
binding.buttonExport.setOnClickListener { installable.export.invoke() } binding.buttonExport.setOnClickListener { model.export.invoke() }
} }
} }
} }

View File

@ -7,49 +7,33 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment import org.yuzu.yuzu_emu.fragments.LicenseBottomSheetDialogFragment
import org.yuzu.yuzu_emu.model.License import org.yuzu.yuzu_emu.model.License
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) : class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(), AbstractListAdapter<License, LicenseAdapter.LicenseViewHolder>(licenses) {
View.OnClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
val binding = ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false) .also { return LicenseViewHolder(it) }
binding.root.setOnClickListener(this)
return LicenseViewHolder(binding)
} }
override fun getItemCount(): Int = licenses.size inner class LicenseViewHolder(val binding: ListItemSettingBinding) :
AbstractViewHolder<License>(binding) {
override fun bind(model: License) {
binding.apply {
textSettingName.text = root.context.getString(model.titleId)
textSettingDescription.text = root.context.getString(model.descriptionId)
textSettingValue.visibility = View.GONE
override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) { root.setOnClickListener { onClick(model) }
holder.bind(licenses[position]) }
}
override fun onClick(view: View) {
val license = (view.tag as LicenseViewHolder).license
LicenseBottomSheetDialogFragment.newInstance(license)
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
}
inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
lateinit var license: License
init {
itemView.tag = this
} }
fun bind(license: License) { private fun onClick(license: License) {
this.license = license LicenseBottomSheetDialogFragment.newInstance(license)
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
val context = YuzuApplication.appContext
binding.textSettingName.text = context.getString(license.titleId)
binding.textSettingDescription.text = context.getString(license.descriptionId)
binding.textSettingValue.visibility = View.GONE
} }
} }
} }

View File

@ -10,7 +10,6 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.yuzu.yuzu_emu.databinding.PageSetupBinding import org.yuzu.yuzu_emu.databinding.PageSetupBinding
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
@ -18,31 +17,19 @@ import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.StepState import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.utils.ViewUtils import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() { AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SetupPageViewHolder(binding) .also { return SetupPageViewHolder(it) }
} }
override fun getItemCount(): Int = pages.size
override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
holder.bind(pages[position])
inner class SetupPageViewHolder(val binding: PageSetupBinding) : inner class SetupPageViewHolder(val binding: PageSetupBinding) :
RecyclerView.ViewHolder(binding.root), SetupCallback { AbstractViewHolder<SetupPage>(binding), SetupCallback {
lateinit var page: SetupPage override fun bind(model: SetupPage) {
if (model.stepCompleted.invoke() == StepState.COMPLETE) {
init {
itemView.tag = this
}
fun bind(page: SetupPage) {
this.page = page
if (page.stepCompleted.invoke() == StepState.COMPLETE) {
binding.buttonAction.visibility = View.INVISIBLE binding.buttonAction.visibility = View.INVISIBLE
binding.textConfirmation.visibility = View.VISIBLE binding.textConfirmation.visibility = View.VISIBLE
} }
@ -50,31 +37,31 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
binding.icon.setImageDrawable( binding.icon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
activity.resources, activity.resources,
page.iconId, model.iconId,
activity.theme activity.theme
) )
) )
binding.textTitle.text = activity.resources.getString(page.titleId) binding.textTitle.text = activity.resources.getString(model.titleId)
binding.textDescription.text = binding.textDescription.text =
Html.fromHtml(activity.resources.getString(page.descriptionId), 0) Html.fromHtml(activity.resources.getString(model.descriptionId), 0)
binding.buttonAction.apply { binding.buttonAction.apply {
text = activity.resources.getString(page.buttonTextId) text = activity.resources.getString(model.buttonTextId)
if (page.buttonIconId != 0) { if (model.buttonIconId != 0) {
icon = ResourcesCompat.getDrawable( icon = ResourcesCompat.getDrawable(
activity.resources, activity.resources,
page.buttonIconId, model.buttonIconId,
activity.theme activity.theme
) )
} }
iconGravity = iconGravity =
if (page.leftAlignedIcon) { if (model.leftAlignedIcon) {
MaterialButton.ICON_GRAVITY_START MaterialButton.ICON_GRAVITY_START
} else { } else {
MaterialButton.ICON_GRAVITY_END MaterialButton.ICON_GRAVITY_END
} }
setOnClickListener { setOnClickListener {
page.buttonAction.invoke(this@SetupPageViewHolder) model.buttonAction.invoke(this@SetupPageViewHolder)
} }
} }
} }

View File

@ -23,7 +23,8 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
THEME("theme"), THEME("theme"),
THEME_MODE("theme_mode"), THEME_MODE("theme_mode"),
OVERLAY_SCALE("control_scale"), OVERLAY_SCALE("control_scale"),
OVERLAY_OPACITY("control_opacity"); OVERLAY_OPACITY("control_opacity"),
LOCK_DRAWER("lock_drawer");
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)

View File

@ -76,8 +76,8 @@ class AboutFragment : Fragment() {
binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment) binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
} }
binding.textBuildHash.text = BuildConfig.GIT_HASH binding.textVersionName.text = BuildConfig.VERSION_NAME
binding.buttonBuildHash.setOnClickListener { binding.buttonVersionName.setOnClickListener {
val clipBoard = val clipBoard =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(getString(R.string.build), BuildConfig.GIT_HASH) val clip = ClipData.newPlainText(getString(R.string.build), BuildConfig.GIT_HASH)

View File

@ -74,7 +74,7 @@ class AddonsFragment : Fragment() {
binding.listAddons.apply { binding.listAddons.apply {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
adapter = AddonAdapter() adapter = AddonAdapter(addonViewModel)
} }
viewLifecycleOwner.lifecycleScope.apply { viewLifecycleOwner.lifecycleScope.apply {
@ -110,6 +110,21 @@ class AddonsFragment : Fragment() {
} }
} }
} }
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.addonToDelete.collect {
if (it != null) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.confirm_uninstall,
descriptionId = R.string.confirm_uninstall_description,
positiveAction = { addonViewModel.onDeleteAddon(it) }
).show(parentFragmentManager, MessageDialogFragment.TAG)
addonViewModel.setAddonToDelete(null)
}
}
}
}
} }
binding.buttonInstall.setOnClickListener { binding.buttonInstall.setOnClickListener {
@ -156,22 +171,22 @@ class AddonsFragment : Fragment() {
descriptionId = R.string.invalid_directory_description descriptionId = R.string.invalid_directory_description
) )
if (isValid) { if (isValid) {
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
requireActivity(), requireActivity(),
R.string.installing_game_content, R.string.installing_game_content,
false false
) { ) { progressCallback, _ ->
val parentDirectoryName = externalAddonDirectory.name val parentDirectoryName = externalAddonDirectory.name
val internalAddonDirectory = val internalAddonDirectory =
File(args.game.addonDir + parentDirectoryName) File(args.game.addonDir + parentDirectoryName)
try { try {
externalAddonDirectory.copyFilesTo(internalAddonDirectory) externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback)
} catch (_: Exception) { } catch (_: Exception) {
return@newInstance errorMessage return@newInstance errorMessage
} }
addonViewModel.refreshAddons() addonViewModel.refreshAddons()
return@newInstance getString(R.string.addon_installed_successfully) return@newInstance getString(R.string.addon_installed_successfully)
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(parentFragmentManager, ProgressDialogFragment.TAG)
} else { } else {
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
} }

View File

@ -3,6 +3,7 @@
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -13,20 +14,26 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.DriverAdapter import org.yuzu.yuzu_emu.adapters.DriverAdapter
import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
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.NativeConfig
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -55,12 +62,43 @@ class DriverManagerFragment : 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?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true) homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
driverViewModel.onOpenDriverManager(args.game) driverViewModel.onOpenDriverManager(args.game)
if (NativeConfig.isPerGameConfigLoaded()) {
binding.toolbarDrivers.inflateMenu(R.menu.menu_driver_manager)
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
binding.toolbarDrivers.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_driver_use_global -> {
StringSetting.DRIVER_PATH.global = true
driverViewModel.updateDriverList()
(binding.listDrivers.adapter as DriverAdapter)
.replaceList(driverViewModel.driverList.value)
driverViewModel.showClearButton(false)
true
}
else -> false
}
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
driverViewModel.showClearButton.collect {
binding.toolbarDrivers.menu
.findItem(R.id.menu_driver_use_global).isVisible = it
}
}
}
}
}
if (!driverViewModel.isInteractionAllowed.value) { if (!driverViewModel.isInteractionAllowed.value) {
DriversLoadingDialogFragment().show( DriversLoadingDialogFragment().show(
@ -85,25 +123,6 @@ class DriverManagerFragment : Fragment() {
adapter = DriverAdapter(driverViewModel) adapter = DriverAdapter(driverViewModel)
} }
viewLifecycleOwner.lifecycleScope.apply {
launch {
driverViewModel.driverList.collectLatest {
(binding.listDrivers.adapter as DriverAdapter).submitList(it)
}
}
launch {
driverViewModel.newDriverInstalled.collect {
if (_binding != null && it) {
(binding.listDrivers.adapter as DriverAdapter).apply {
notifyItemChanged(driverViewModel.previouslySelectedDriver)
notifyItemChanged(driverViewModel.selectedDriver)
driverViewModel.setNewDriverInstalled(false)
}
}
}
}
}
setInsets() setInsets()
} }
@ -154,13 +173,13 @@ class DriverManagerFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
requireActivity(), requireActivity(),
R.string.installing_driver, R.string.installing_driver,
false false
) { ) { _, _ ->
val driverPath = val driverPath =
"${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}" "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
val driverFile = File(driverPath) val driverFile = File(driverPath)
// Ignore file exceptions when a user selects an invalid zip // Ignore file exceptions when a user selects an invalid zip
@ -177,14 +196,23 @@ class DriverManagerFragment : Fragment() {
val driverData = GpuDriverHelper.getMetadataFromZip(driverFile) val driverData = GpuDriverHelper.getMetadataFromZip(driverFile)
val driverInList = val driverInList =
driverViewModel.driverList.value.firstOrNull { it.second == driverData } driverViewModel.driverData.firstOrNull { it.second == driverData }
if (driverInList != null) { if (driverInList != null) {
return@newInstance getString(R.string.driver_already_installed) return@newInstance getString(R.string.driver_already_installed)
} else { } else {
driverViewModel.addDriver(Pair(driverPath, driverData)) driverViewModel.onDriverAdded(Pair(driverPath, driverData))
driverViewModel.setNewDriverInstalled(true) withContext(Dispatchers.Main) {
if (_binding != null) {
val adapter = binding.listDrivers.adapter as DriverAdapter
adapter.addItem(driverData.toDriver())
adapter.selectItem(adapter.currentList.indices.last)
driverViewModel.showClearButton(!StringSetting.DRIVER_PATH.global)
binding.listDrivers
.smoothScrollToPosition(adapter.currentList.indices.last)
}
}
} }
return@newInstance Any() return@newInstance Any()
}.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(childFragmentManager, ProgressDialogFragment.TAG)
} }
} }

View File

@ -141,7 +141,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// 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
emulationState = EmulationState(game.path) emulationState = EmulationState(game.path) {
return@EmulationState driverViewModel.isInteractionAllowed.value
}
} }
/** /**
@ -182,11 +184,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
override fun onDrawerOpened(drawerView: View) { override fun onDrawerOpened(drawerView: View) {
// No op binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
binding.inGameMenu.requestFocus()
emulationViewModel.setDrawerOpen(true)
} }
override fun onDrawerClosed(drawerView: View) { override fun onDrawerClosed(drawerView: View) {
// No op binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt())
emulationViewModel.setDrawerOpen(false)
} }
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
@ -196,6 +201,28 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text = binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
game.title game.title
binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
val lockMode = IntSetting.LOCK_DRAWER.getInt()
val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
R.string.unlock_drawer
} else {
R.string.lock_drawer
}
val iconId = if (lockMode == DrawerLayout.LOCK_MODE_UNLOCKED) {
R.drawable.ic_unlock
} else {
R.drawable.ic_lock
}
title = getString(titleId)
icon = ResourcesCompat.getDrawable(
resources,
iconId,
requireContext().theme
)
}
binding.inGameMenu.setNavigationItemSelectedListener { binding.inGameMenu.setNavigationItemSelectedListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_pause_emulation -> { R.id.menu_pause_emulation -> {
@ -216,6 +243,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
requireContext().theme requireContext().theme
) )
} }
binding.inGameMenu.requestFocus()
true true
} }
@ -224,6 +252,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
null, null,
Settings.MenuTag.SECTION_ROOT Settings.MenuTag.SECTION_ROOT
) )
binding.inGameMenu.requestFocus()
binding.root.findNavController().navigate(action) binding.root.findNavController().navigate(action)
true true
} }
@ -233,6 +262,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
args.game, args.game,
Settings.MenuTag.SECTION_ROOT Settings.MenuTag.SECTION_ROOT
) )
binding.inGameMenu.requestFocus()
binding.root.findNavController().navigate(action) binding.root.findNavController().navigate(action)
true true
} }
@ -242,11 +272,39 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true true
} }
R.id.menu_lock_drawer -> {
when (IntSetting.LOCK_DRAWER.getInt()) {
DrawerLayout.LOCK_MODE_UNLOCKED -> {
IntSetting.LOCK_DRAWER.setInt(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
it.title = resources.getString(R.string.unlock_drawer)
it.icon = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_lock,
requireContext().theme
)
}
DrawerLayout.LOCK_MODE_LOCKED_CLOSED -> {
IntSetting.LOCK_DRAWER.setInt(DrawerLayout.LOCK_MODE_UNLOCKED)
it.title = resources.getString(R.string.lock_drawer)
it.icon = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_unlock,
requireContext().theme
)
}
}
binding.inGameMenu.requestFocus()
NativeConfig.saveGlobalConfig()
true
}
R.id.menu_exit -> { R.id.menu_exit -> {
emulationState.stop() emulationState.stop()
NativeConfig.reloadGlobalConfig()
emulationViewModel.setIsEmulationStopping(true) emulationViewModel.setIsEmulationStopping(true)
binding.drawerLayout.close() binding.drawerLayout.close()
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) binding.inGameMenu.requestFocus()
true true
} }
@ -263,12 +321,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
if (!NativeLibrary.isRunning()) { if (!NativeLibrary.isRunning()) {
return return
} }
emulationViewModel.setDrawerOpen(!binding.drawerLayout.isOpen)
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
} else {
binding.drawerLayout.open()
}
} }
} }
) )
@ -322,11 +375,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
} }
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.isInteractionAllowed.collect {
if (it) {
startEmulation()
}
}
}
}
launch { launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
emulationViewModel.emulationStarted.collectLatest { emulationViewModel.emulationStarted.collectLatest {
if (it) { if (it) {
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) binding.drawerLayout.setDrawerLockMode(IntSetting.LOCK_DRAWER.getInt())
ViewUtils.showView(binding.surfaceInputOverlay) ViewUtils.showView(binding.surfaceInputOverlay)
ViewUtils.hideView(binding.loadingIndicator) ViewUtils.hideView(binding.loadingIndicator)
@ -351,10 +413,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
launch { launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
driverViewModel.isInteractionAllowed.collect { emulationViewModel.drawerOpen.collect {
if (it) { if (it) {
onEmulationStart() binding.drawerLayout.open()
binding.inGameMenu.requestFocus()
} else {
binding.drawerLayout.close()
} }
} }
} }
@ -362,7 +427,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
private fun onEmulationStart() { private fun startEmulation() {
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
if (!DirectoryInitialization.areDirectoriesReady) { if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start() DirectoryInitialization.start()
@ -437,12 +502,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val FRAMETIME = 2 val FRAMETIME = 2
val SPEED = 3 val SPEED = 3
perfStatsUpdater = { perfStatsUpdater = {
if (emulationViewModel.emulationStarted.value) { if (emulationViewModel.emulationStarted.value &&
!emulationViewModel.isEmulationStopping.value
) {
val perfStats = NativeLibrary.getPerfStats() val perfStats = NativeLibrary.getPerfStats()
val cpuBackend = NativeLibrary.getCpuBackend() val cpuBackend = NativeLibrary.getCpuBackend()
val gpuDriver = NativeLibrary.getGpuDriver()
if (_binding != null) { if (_binding != null) {
binding.showFpsText.text = binding.showFpsText.text =
String.format("FPS: %.1f\n%s", perfStats[FPS], cpuBackend) String.format("FPS: %.1f\n%s/%s", perfStats[FPS], cpuBackend, gpuDriver)
} }
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800) perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800)
} }
@ -554,6 +622,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean() findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean()
} }
popup.setOnDismissListener { NativeConfig.saveGlobalConfig() }
popup.setOnMenuItemClickListener { popup.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.menu_toggle_fps -> { R.id.menu_toggle_fps -> {
@ -720,7 +789,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.emulation_control_adjust) .setTitle(R.string.emulation_control_adjust)
.setView(adjustBinding.root) .setView(adjustBinding.root)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
NativeConfig.saveGlobalConfig()
}
.setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int -> .setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
setControlScale(50) setControlScale(50)
setControlOpacity(100) setControlOpacity(100)
@ -756,7 +827,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
private class EmulationState(private val gamePath: String) { private class EmulationState(
private val gamePath: String,
private val emulationCanStart: () -> Boolean
) {
private var state: State private var state: State
private var surface: Surface? = null private var surface: Surface? = null
@ -850,6 +924,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
State.PAUSED -> Log.warning( State.PAUSED -> Log.warning(
"[EmulationFragment] Surface cleared while emulation paused." "[EmulationFragment] Surface cleared while emulation paused."
) )
else -> Log.warning( else -> Log.warning(
"[EmulationFragment] Surface cleared while emulation stopped." "[EmulationFragment] Surface cleared while emulation stopped."
) )
@ -859,6 +934,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private fun runWithValidSurface() { private fun runWithValidSurface() {
NativeLibrary.surfaceChanged(surface) NativeLibrary.surfaceChanged(surface)
if (!emulationCanStart.invoke()) {
return
}
when (state) { when (state) {
State.STOPPED -> { State.STOPPED -> {
val emulationThread = Thread({ val emulationThread = Thread({

View File

@ -21,8 +21,10 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
import org.yuzu.yuzu_emu.model.GameVerificationResult
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.GameMetadata import org.yuzu.yuzu_emu.utils.GameMetadata
@ -101,6 +103,38 @@ class GameInfoFragment : Fragment() {
""".trimIndent() """.trimIndent()
copyToClipboard(args.game.title, details) copyToClipboard(args.game.title, details)
} }
buttonVerifyIntegrity.setOnClickListener {
ProgressDialogFragment.newInstance(
requireActivity(),
R.string.verifying,
true
) { progressCallback, _ ->
val result = GameVerificationResult.from(
NativeLibrary.verifyGameContents(
args.game.path,
progressCallback
)
)
return@newInstance when (result) {
GameVerificationResult.Success ->
MessageDialogFragment.newInstance(
titleId = R.string.verify_success,
descriptionId = R.string.operation_completed_successfully
)
GameVerificationResult.Failed ->
MessageDialogFragment.newInstance(
titleId = R.string.verify_failure,
descriptionId = R.string.verify_failure_description
)
GameVerificationResult.NotImplemented ->
MessageDialogFragment.newInstance(
titleId = R.string.verify_no_result,
descriptionId = R.string.verify_no_result_description
)
}
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
}
} }
setInsets() setInsets()

View File

@ -4,6 +4,8 @@
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
@ -44,7 +46,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.MemoryUtil import org.yuzu.yuzu_emu.utils.MemoryUtil
import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
@ -85,6 +86,24 @@ class GamePropertiesFragment : Fragment() {
view.findNavController().popBackStack() view.findNavController().popBackStack()
} }
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java)
binding.buttonShortcut.isEnabled = shortcutManager.isRequestPinShortcutSupported
binding.buttonShortcut.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
val shortcut = ShortcutInfo.Builder(requireContext(), args.game.title)
.setShortLabel(args.game.title)
.setIcon(
GameIconUtils.getShortcutIcon(requireActivity(), args.game)
.toIcon(requireContext())
)
.setIntent(args.game.launchIntent)
.build()
shortcutManager.requestPinShortcut(shortcut, null)
}
}
}
GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
binding.title.text = args.game.title binding.title.text = args.game.title
binding.title.postDelayed( binding.title.postDelayed(
@ -357,27 +376,17 @@ class GamePropertiesFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
val inputZip = requireContext().contentResolver.openInputStream(result)
val savesFolder = File(args.game.saveDir) val savesFolder = File(args.game.saveDir)
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
cacheSaveDir.mkdir() cacheSaveDir.mkdir()
if (inputZip == null) { ProgressDialogFragment.newInstance(
Toast.makeText(
YuzuApplication.appContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
return@registerForActivityResult
}
IndeterminateProgressDialogFragment.newInstance(
requireActivity(), requireActivity(),
R.string.save_files_importing, R.string.save_files_importing,
false false
) { ) { _, _ ->
try { try {
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir)
val files = cacheSaveDir.listFiles() val files = cacheSaveDir.listFiles()
var savesFolderFile: File? = null var savesFolderFile: File? = null
if (files != null) { if (files != null) {
@ -422,7 +431,7 @@ class GamePropertiesFragment : Fragment() {
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(parentFragmentManager, ProgressDialogFragment.TAG)
} }
/** /**
@ -436,11 +445,11 @@ class GamePropertiesFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
requireActivity(), requireActivity(),
R.string.save_files_exporting, R.string.save_files_exporting,
false false
) { ) { _, _ ->
val saveLocation = args.game.saveDir val saveLocation = args.game.saveDir
val zipResult = FileUtil.zipFromInternalStorage( val zipResult = FileUtil.zipFromInternalStorage(
File(saveLocation), File(saveLocation),
@ -452,6 +461,6 @@ class GamePropertiesFragment : Fragment() {
TaskState.Completed -> getString(R.string.export_success) TaskState.Completed -> getString(R.string.export_success)
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
} }
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(parentFragmentManager, ProgressDialogFragment.TAG)
} }
} }

View File

@ -32,6 +32,7 @@ import org.yuzu.yuzu_emu.BuildConfig
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.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.DocumentProvider
@ -140,6 +141,44 @@ class HomeSettingsFragment : Fragment() {
} }
) )
) )
add(
HomeSetting(
R.string.verify_installed_content,
R.string.verify_installed_content_description,
R.drawable.ic_check_circle,
{
ProgressDialogFragment.newInstance(
requireActivity(),
titleId = R.string.verifying,
cancellable = true
) { progressCallback, _ ->
val result = NativeLibrary.verifyInstalledContents(progressCallback)
return@newInstance if (progressCallback.invoke(100, 100)) {
// Invoke the progress callback to check if the process was cancelled
MessageDialogFragment.newInstance(
titleId = R.string.verify_no_result,
descriptionId = R.string.verify_no_result_description
)
} else if (result.isEmpty()) {
MessageDialogFragment.newInstance(
titleId = R.string.verify_success,
descriptionId = R.string.operation_completed_successfully
)
} else {
val failedNames = result.joinToString("\n")
val errorMessage = YuzuApplication.appContext.getString(
R.string.verification_failed_for,
failedNames
)
MessageDialogFragment.newInstance(
titleId = R.string.verify_failure,
descriptionString = errorMessage
)
}
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
}
)
)
add( add(
HomeSetting( HomeSetting(
R.string.share_log, R.string.share_log,

View File

@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.ui.main.MainActivity
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.FileUtil
import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import java.math.BigInteger import java.math.BigInteger
@ -195,26 +194,20 @@ class InstallableFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
val inputZip = requireContext().contentResolver.openInputStream(result)
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
cacheSaveDir.mkdir() cacheSaveDir.mkdir()
if (inputZip == null) { ProgressDialogFragment.newInstance(
Toast.makeText(
YuzuApplication.appContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
return@registerForActivityResult
}
IndeterminateProgressDialogFragment.newInstance(
requireActivity(), requireActivity(),
R.string.save_files_importing, R.string.save_files_importing,
false false
) { ) { progressCallback, _ ->
try { try {
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) FileUtil.unzipToInternalStorage(
result.toString(),
cacheSaveDir,
progressCallback
)
val files = cacheSaveDir.listFiles() val files = cacheSaveDir.listFiles()
var successfulImports = 0 var successfulImports = 0
var failedImports = 0 var failedImports = 0
@ -287,7 +280,7 @@ class InstallableFragment : Fragment() {
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(parentFragmentManager, ProgressDialogFragment.TAG)
} }
private val exportSaves = registerForActivityResult( private val exportSaves = registerForActivityResult(
@ -297,11 +290,11 @@ class InstallableFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
requireActivity(), requireActivity(),
R.string.save_files_exporting, R.string.save_files_exporting,
false false
) { ) { _, _ ->
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
cacheSaveDir.mkdir() cacheSaveDir.mkdir()
@ -338,6 +331,6 @@ class InstallableFragment : Fragment() {
TaskState.Completed -> getString(R.string.export_success) TaskState.Completed -> getString(R.string.export_success)
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
} }
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(parentFragmentManager, ProgressDialogFragment.TAG)
} }
} }

View File

@ -26,9 +26,15 @@ class MessageDialogFragment : DialogFragment() {
val descriptionId = requireArguments().getInt(DESCRIPTION_ID) val descriptionId = requireArguments().getInt(DESCRIPTION_ID)
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 dismissible = requireArguments().getBoolean(DISMISSIBLE)
val clearPositiveAction = requireArguments().getBoolean(CLEAR_POSITIVE_ACTION)
val builder = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())
if (clearPositiveAction) {
messageDialogViewModel.positiveAction = null
}
if (messageDialogViewModel.positiveAction == null) { if (messageDialogViewModel.positiveAction == null) {
builder.setPositiveButton(R.string.close, null) builder.setPositiveButton(R.string.close, null)
} else { } else {
@ -51,6 +57,8 @@ class MessageDialogFragment : DialogFragment() {
} }
} }
isCancelable = dismissible
return builder.show() return builder.show()
} }
@ -67,28 +75,38 @@ class MessageDialogFragment : DialogFragment() {
private const val DESCRIPTION_ID = "DescriptionId" private const val DESCRIPTION_ID = "DescriptionId"
private const val DESCRIPTION_STRING = "DescriptionString" private const val DESCRIPTION_STRING = "DescriptionString"
private const val HELP_LINK = "Link" private const val HELP_LINK = "Link"
private const val DISMISSIBLE = "Dismissible"
private const val CLEAR_POSITIVE_ACTION = "ClearPositiveAction"
fun newInstance( fun newInstance(
activity: FragmentActivity, activity: FragmentActivity? = null,
titleId: Int = 0, titleId: Int = 0,
titleString: String = "", titleString: String = "",
descriptionId: Int = 0, descriptionId: Int = 0,
descriptionString: String = "", descriptionString: String = "",
helpLinkId: Int = 0, helpLinkId: Int = 0,
dismissible: Boolean = true,
positiveAction: (() -> Unit)? = null positiveAction: (() -> Unit)? = null
): MessageDialogFragment { ): MessageDialogFragment {
var clearPositiveAction = false
if (activity != null) {
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
clear()
this.positiveAction = positiveAction
}
} else {
clearPositiveAction = true
}
val dialog = MessageDialogFragment() val dialog = MessageDialogFragment()
val bundle = Bundle() val bundle = Bundle().apply {
bundle.apply {
putInt(TITLE_ID, titleId) putInt(TITLE_ID, titleId)
putString(TITLE_STRING, titleString) putString(TITLE_STRING, titleString)
putInt(DESCRIPTION_ID, descriptionId) putInt(DESCRIPTION_ID, descriptionId)
putString(DESCRIPTION_STRING, descriptionString) putString(DESCRIPTION_STRING, descriptionString)
putInt(HELP_LINK, helpLinkId) putInt(HELP_LINK, helpLinkId)
} putBoolean(DISMISSIBLE, dismissible)
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply { putBoolean(CLEAR_POSITIVE_ACTION, clearPositiveAction)
clear()
this.positiveAction = positiveAction
} }
dialog.arguments = bundle dialog.arguments = bundle
return dialog return dialog

View File

@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.model.TaskViewModel
class IndeterminateProgressDialogFragment : DialogFragment() { class ProgressDialogFragment : DialogFragment() {
private val taskViewModel: TaskViewModel by activityViewModels() private val taskViewModel: TaskViewModel by activityViewModels()
private lateinit var binding: DialogProgressBarBinding private lateinit var binding: DialogProgressBarBinding
private val PROGRESS_BAR_RESOLUTION = 1000
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val titleId = requireArguments().getInt(TITLE) val titleId = requireArguments().getInt(TITLE)
val cancellable = requireArguments().getBoolean(CANCELLABLE) val cancellable = requireArguments().getBoolean(CANCELLABLE)
@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.message.isSelected = true
viewLifecycleOwner.lifecycleScope.apply { viewLifecycleOwner.lifecycleScope.apply {
launch { launch {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
} }
} }
} }
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
taskViewModel.progress.collect {
if (it != 0.0) {
binding.progressBar.apply {
isIndeterminate = false
progress = (
(it / taskViewModel.maxProgress.value) *
PROGRESS_BAR_RESOLUTION
).toInt()
min = 0
max = PROGRESS_BAR_RESOLUTION
}
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
taskViewModel.message.collect {
if (it.isEmpty()) {
binding.message.visibility = View.GONE
} else {
binding.message.visibility = View.VISIBLE
binding.message.text = it
}
}
}
}
} }
} }
@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
negativeButton.setOnClickListener { negativeButton.setOnClickListener {
alertDialog.setTitle(getString(R.string.cancelling)) alertDialog.setTitle(getString(R.string.cancelling))
binding.progressBar.isIndeterminate = true
taskViewModel.setCancelled(true) taskViewModel.setCancelled(true)
} }
} }
@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
activity: FragmentActivity, activity: FragmentActivity,
titleId: Int, titleId: Int,
cancellable: Boolean = false, cancellable: Boolean = false,
task: suspend () -> Any task: suspend (
): IndeterminateProgressDialogFragment { progressCallback: (max: Long, progress: Long) -> Boolean,
val dialog = IndeterminateProgressDialogFragment() messageCallback: (message: String) -> Unit
) -> Any
): ProgressDialogFragment {
val dialog = ProgressDialogFragment()
val args = Bundle() val args = Bundle()
ViewModelProvider(activity)[TaskViewModel::class.java].task = task ViewModelProvider(activity)[TaskViewModel::class.java].task = task
args.putInt(TITLE, titleId) args.putInt(TITLE, titleId)

View File

@ -136,14 +136,14 @@ class SearchFragment : Fragment() {
baseList.filter { baseList.filter {
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L) val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000) lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
} }.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
} }
R.id.chip_recently_added -> { R.id.chip_recently_added -> {
baseList.filter { baseList.filter {
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L) val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000) addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
} }.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
} }
R.id.chip_homebrew -> baseList.filter { it.isHomebrew } R.id.chip_homebrew -> baseList.filter { it.isHomebrew }

View File

@ -31,6 +31,7 @@ import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary
import java.io.File import java.io.File
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
@ -162,7 +163,7 @@ class SetupFragment : Fragment() {
R.string.install_prod_keys_warning_help, R.string.install_prod_keys_warning_help,
{ {
val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys") val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys")
if (file.exists()) { if (file.exists() && NativeLibrary.areKeysPresent()) {
StepState.COMPLETE StepState.COMPLETE
} else { } else {
StepState.INCOMPLETE StepState.INCOMPLETE
@ -347,7 +348,8 @@ class SetupFragment : Fragment() {
val getProdKey = val getProdKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) { if (result != null) {
if (mainActivity.processKey(result)) { mainActivity.processKey(result)
if (NativeLibrary.areKeysPresent()) {
keyCallback.onStepCompleted() keyCallback.onStepCompleted()
} }
} }

View File

@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
data class Addon(
var enabled: Boolean,
val title: String,
val version: String
)

View File

@ -15,8 +15,8 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class AddonViewModel : ViewModel() { class AddonViewModel : ViewModel() {
private val _addonList = MutableStateFlow(mutableListOf<Addon>()) private val _patchList = MutableStateFlow(mutableListOf<Patch>())
val addonList get() = _addonList.asStateFlow() val addonList get() = _patchList.asStateFlow()
private val _showModInstallPicker = MutableStateFlow(false) private val _showModInstallPicker = MutableStateFlow(false)
val showModInstallPicker get() = _showModInstallPicker.asStateFlow() val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() {
private val _showModNoticeDialog = MutableStateFlow(false) private val _showModNoticeDialog = MutableStateFlow(false)
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
private val _addonToDelete = MutableStateFlow<Patch?>(null)
val addonToDelete = _addonToDelete.asStateFlow()
var game: Game? = null var game: Game? = null
private val isRefreshing = AtomicBoolean(false) private val isRefreshing = AtomicBoolean(false)
@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() {
isRefreshing.set(true) isRefreshing.set(true)
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val addonList = mutableListOf<Addon>() val patchList = (
val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId) NativeLibrary.getPatchesForFile(game!!.path, game!!.programId)
NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach { ?: emptyArray()
val name = it.first.replace("[D] ", "") ).toMutableList()
addonList.add(Addon(!disabledAddons.contains(name), name, it.second)) patchList.sortBy { it.name }
} _patchList.value = patchList
addonList.sortBy { it.title }
_addonList.value = addonList
isRefreshing.set(false) isRefreshing.set(false)
} }
} }
} }
fun setAddonToDelete(patch: Patch?) {
_addonToDelete.value = patch
}
fun onDeleteAddon(patch: Patch) {
when (PatchType.from(patch.type)) {
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
PatchType.DLC -> NativeLibrary.removeDLC(patch.programId)
PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name)
}
refreshAddons()
}
fun onCloseAddons() { fun onCloseAddons() {
if (_addonList.value.isEmpty()) { if (_patchList.value.isEmpty()) {
return return
} }
NativeConfig.setDisabledAddons( NativeConfig.setDisabledAddons(
game!!.programId, game!!.programId,
_addonList.value.mapNotNull { _patchList.value.mapNotNull {
if (it.enabled) { if (it.enabled) {
null null
} else { } else {
it.title it.name
} }
}.toTypedArray() }.toTypedArray()
) )
NativeConfig.saveGlobalConfig() NativeConfig.saveGlobalConfig()
_addonList.value.clear() _patchList.value.clear()
game = null game = null
} }

View File

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
data class Driver(
override var selected: Boolean,
val title: String,
val version: String = "",
val description: String = ""
) : SelectableItem {
override fun onSelectionStateChanged(selected: Boolean) {
this.selected = selected
}
companion object {
fun GpuDriverMetadata.toDriver(selected: Boolean = false): Driver =
Driver(
selected,
this.name ?: "",
this.version ?: "",
this.description ?: ""
)
}
}

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -17,11 +18,10 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
import java.io.BufferedOutputStream
import java.io.File import java.io.File
class DriverViewModel : ViewModel() { class DriverViewModel : ViewModel() {
@ -38,97 +38,81 @@ class DriverViewModel : ViewModel() {
!loading && ready && !deleting !loading && ready && !deleting
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers()) var driverData = GpuDriverHelper.getDrivers()
val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
var previouslySelectedDriver = 0 private val _driverList = MutableStateFlow(emptyList<Driver>())
var selectedDriver = -1 val driverList: StateFlow<List<Driver>> get() = _driverList
// Used for showing which driver is currently installed within the driver manager card // Used for showing which driver is currently installed within the driver manager card
private val _selectedDriverTitle = MutableStateFlow("") private val _selectedDriverTitle = MutableStateFlow("")
val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
private val _newDriverInstalled = MutableStateFlow(false) private val _showClearButton = MutableStateFlow(false)
val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled val showClearButton = _showClearButton.asStateFlow()
val driversToDelete = mutableListOf<String>() private val driversToDelete = mutableListOf<String>()
init { init {
val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData updateDriverList()
findSelectedDriver(currentDriverMetadata)
// If a user had installed a driver before the manager was implemented, this zips
// the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
// be indexed and exported as expected.
if (selectedDriver == -1) {
val driverToSave =
File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
driverToSave.createNewFile()
FileUtil.zipFromInternalStorage(
File(GpuDriverHelper.driverInstallationPath!!),
GpuDriverHelper.driverInstallationPath!!,
BufferedOutputStream(driverToSave.outputStream())
)
_driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
setSelectedDriverIndex(_driverList.value.size - 1)
}
// If a user had installed a driver before the config was reworked to be multiplatform,
// we have save the path of the previously selected driver to the new setting.
if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 &&
StringSetting.DRIVER_PATH.global
) {
StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first)
NativeConfig.saveGlobalConfig()
} else {
findSelectedDriver(GpuDriverHelper.customDriverSettingData)
}
updateDriverNameForGame(null) updateDriverNameForGame(null)
} }
fun setSelectedDriverIndex(value: Int) { fun reloadDriverData() {
if (selectedDriver != -1) { _areDriversLoading.value = true
previouslySelectedDriver = selectedDriver driverData = GpuDriverHelper.getDrivers()
updateDriverList()
_areDriversLoading.value = false
}
fun updateDriverList() {
val selectedDriver = GpuDriverHelper.customDriverSettingData
val newDriverList = mutableListOf(
Driver(
selectedDriver == GpuDriverMetadata(),
YuzuApplication.appContext.getString(R.string.system_gpu_driver)
)
)
driverData.forEach {
newDriverList.add(it.second.toDriver(it.second == selectedDriver))
} }
selectedDriver = value _driverList.value = newDriverList
}
fun setNewDriverInstalled(value: Boolean) {
_newDriverInstalled.value = value
}
fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
val driverIndex = _driverList.value.indexOfFirst { it == driverData }
if (driverIndex == -1) {
_driverList.value.add(driverData)
setSelectedDriverIndex(_driverList.value.size - 1)
_selectedDriverTitle.value = driverData.second.name
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
} else {
setSelectedDriverIndex(driverIndex)
}
}
fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) {
_driverList.value.remove(driverData)
} }
fun onOpenDriverManager(game: Game?) { fun onOpenDriverManager(game: Game?) {
if (game != null) { if (game != null) {
SettingsFile.loadCustomConfig(game) SettingsFile.loadCustomConfig(game)
} }
updateDriverList()
}
val driverPath = StringSetting.DRIVER_PATH.getString() fun showClearButton(value: Boolean) {
if (driverPath.isEmpty()) { _showClearButton.value = value
setSelectedDriverIndex(0) }
fun onDriverSelected(position: Int) {
if (position == 0) {
StringSetting.DRIVER_PATH.setString("")
} else { } else {
findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath))) StringSetting.DRIVER_PATH.setString(driverData[position - 1].first)
} }
} }
fun onDriverRemoved(removedPosition: Int, selectedPosition: Int) {
driversToDelete.add(driverData[removedPosition - 1].first)
driverData.removeAt(removedPosition - 1)
onDriverSelected(selectedPosition)
}
fun onDriverAdded(driver: Pair<String, GpuDriverMetadata>) {
if (driversToDelete.contains(driver.first)) {
driversToDelete.remove(driver.first)
}
driverData.add(driver)
onDriverSelected(driverData.size)
}
fun onCloseDriverManager(game: Game?) { fun onCloseDriverManager(game: Game?) {
_isDeletingDrivers.value = true _isDeletingDrivers.value = true
StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
updateDriverNameForGame(game) updateDriverNameForGame(game)
if (game == null) { if (game == null) {
NativeConfig.saveGlobalConfig() NativeConfig.saveGlobalConfig()
@ -160,6 +144,7 @@ class DriverViewModel : ViewModel() {
val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString()) val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString())
val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData
if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) { if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) {
setDriverReady()
return return
} }
@ -181,20 +166,6 @@ class DriverViewModel : ViewModel() {
} }
} }
private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) {
if (driverList.value.size == 1) {
setSelectedDriverIndex(0)
return
}
driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> ->
if (driver.second == currentDriverMetadata) {
setSelectedDriverIndex(i)
return
}
}
}
fun updateDriverNameForGame(game: Game?) { fun updateDriverNameForGame(game: Game?) {
if (!GpuDriverHelper.supportsCustomDriverLoading()) { if (!GpuDriverHelper.supportsCustomDriverLoading()) {
return return
@ -217,7 +188,6 @@ class DriverViewModel : ViewModel() {
private fun setDriverReady() { private fun setDriverReady() {
_isDriverReady.value = true _isDriverReady.value = true
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name updateName()
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
} }
} }

View File

@ -6,6 +6,7 @@ package org.yuzu.yuzu_emu.model
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 EmulationViewModel : ViewModel() { class EmulationViewModel : ViewModel() {
val emulationStarted: StateFlow<Boolean> get() = _emulationStarted val emulationStarted: StateFlow<Boolean> get() = _emulationStarted
@ -23,6 +24,9 @@ class EmulationViewModel : ViewModel() {
val shaderMessage: StateFlow<String> get() = _shaderMessage val shaderMessage: StateFlow<String> get() = _shaderMessage
private val _shaderMessage = MutableStateFlow("") private val _shaderMessage = MutableStateFlow("")
private val _drawerOpen = MutableStateFlow(false)
val drawerOpen = _drawerOpen.asStateFlow()
fun setEmulationStarted(started: Boolean) { fun setEmulationStarted(started: Boolean) {
_emulationStarted.value = started _emulationStarted.value = started
} }
@ -49,6 +53,10 @@ class EmulationViewModel : ViewModel() {
setTotalShaders(max) setTotalShaders(max)
} }
fun setDrawerOpen(value: Boolean) {
_drawerOpen.value = value
}
fun clear() { fun clear() {
setEmulationStarted(false) setEmulationStarted(false)
setIsEmulationStopping(false) setIsEmulationStopping(false)

View File

@ -3,6 +3,7 @@
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import java.util.HashSet import java.util.HashSet
@ -11,6 +12,7 @@ import kotlinx.serialization.Serializable
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.activities.EmulationActivity
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.FileUtil
import java.time.LocalDateTime import java.time.LocalDateTime
@ -61,12 +63,26 @@ class Game(
val addonDir: String val addonDir: String
get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/" get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
override fun equals(other: Any?): Boolean { val launchIntent: Intent
if (other !is Game) { get() = Intent(YuzuApplication.appContext, EmulationActivity::class.java).apply {
return false action = Intent.ACTION_VIEW
data = Uri.parse(path)
} }
return hashCode() == other.hashCode() override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Game
if (title != other.title) return false
if (path != other.path) return false
if (programId != other.programId) return false
if (developer != other.developer) return false
if (version != other.version) return false
if (isHomebrew != other.isHomebrew) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
enum class GameVerificationResult(val int: Int) {
Success(0),
Failed(1),
NotImplemented(2);
companion object {
fun from(int: Int): GameVerificationResult =
entries.firstOrNull { it.int == int } ?: Success
}
}

View File

@ -31,6 +31,9 @@ class HomeViewModel : ViewModel() {
private val _reloadPropertiesList = MutableStateFlow(false) private val _reloadPropertiesList = MutableStateFlow(false)
val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow() val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
private val _checkKeys = MutableStateFlow(false)
val checkKeys = _checkKeys.asStateFlow()
var navigatedToSetup = false var navigatedToSetup = false
fun setNavigationVisibility(visible: Boolean, animated: Boolean) { fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@ -66,4 +69,8 @@ class HomeViewModel : ViewModel() {
fun reloadPropertiesList(reload: Boolean) { fun reloadPropertiesList(reload: Boolean) {
_reloadPropertiesList.value = reload _reloadPropertiesList.value = reload
} }
fun setCheckKeys(value: Boolean) {
_checkKeys.value = value
}
} }

View File

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
enum class InstallResult(val int: Int) {
Success(0),
Overwrite(1),
Failure(2),
BaseInstallAttempted(3);
companion object {
fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success
}
}

View File

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
import androidx.annotation.Keep
@Keep
data class Patch(
var enabled: Boolean,
val name: String,
val version: String,
val type: Int,
val programId: String,
val titleId: String
)

View File

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
enum class PatchType(val int: Int) {
Update(0),
DLC(1),
Mod(2);
companion object {
fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update
}
}

View File

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
interface SelectableItem {
var selected: Boolean
fun onSelectionStateChanged(selected: Boolean)
}

View File

@ -8,6 +8,7 @@ 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.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class TaskViewModel : ViewModel() { class TaskViewModel : ViewModel() {
@ -23,13 +24,28 @@ 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: suspend () -> Any private val _progress = MutableStateFlow(0.0)
val progress = _progress.asStateFlow()
private val _maxProgress = MutableStateFlow(0.0)
val maxProgress = _maxProgress.asStateFlow()
private val _message = MutableStateFlow("")
val message = _message.asStateFlow()
lateinit var task: suspend (
progressCallback: (max: Long, progress: Long) -> Boolean,
messageCallback: (message: String) -> Unit
) -> Any
fun clear() { fun clear() {
_result.value = Any() _result.value = Any()
_isComplete.value = false _isComplete.value = false
_isRunning.value = false _isRunning.value = false
_cancelled.value = false _cancelled.value = false
_progress.value = 0.0
_maxProgress.value = 0.0
_message.value = ""
} }
fun setCancelled(value: Boolean) { fun setCancelled(value: Boolean) {
@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() {
_isRunning.value = true _isRunning.value = true
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val res = task() val res = task(
{ max, progress ->
_maxProgress.value = max.toDouble()
_progress.value = progress.toDouble()
return@task cancelled.value
},
{ message ->
_message.value = message
}
)
_result.value = res _result.value = res
_isComplete.value = true _isComplete.value = true
_isRunning.value = false _isRunning.value = false

View File

@ -38,11 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings 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.ProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.AddonViewModel import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.InstallResult
import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
@ -58,9 +60,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
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() private val addonViewModel: AddonViewModel by viewModels()
private val driverViewModel: DriverViewModel by viewModels()
override var themeId: Int = 0 override var themeId: Int = 0
private val CHECKED_DECRYPTION = "CheckedDecryption"
private var checkedDecryption = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@ -72,6 +78,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
if (savedInstanceState != null) {
checkedDecryption = savedInstanceState.getBoolean(CHECKED_DECRYPTION)
}
if (!checkedDecryption) {
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
if (!firstTimeSetup) {
checkKeys()
}
checkedDecryption = true
}
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
@ -147,6 +165,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
} }
} }
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.checkKeys.collect {
if (it) {
checkKeys()
homeViewModel.setCheckKeys(false)
}
}
}
}
} }
// Dismiss previous notifications (should not happen unless a crash occurred) // Dismiss previous notifications (should not happen unless a crash occurred)
@ -155,6 +183,21 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
setInsets() setInsets()
} }
private fun checkKeys() {
if (!NativeLibrary.areKeysPresent()) {
MessageDialogFragment.newInstance(
titleId = R.string.keys_missing,
descriptionId = R.string.keys_missing_description,
helpLinkId = R.string.keys_missing_help
).show(supportFragmentManager, MessageDialogFragment.TAG)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(CHECKED_DECRYPTION, checkedDecryption)
}
fun finishSetup(navController: NavController) { fun finishSetup(navController: NavController) {
navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment) navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
(binding.navigationView as NavigationBarView).setupWithNavController(navController) (binding.navigationView as NavigationBarView).setupWithNavController(navController)
@ -346,6 +389,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
R.string.install_keys_success, R.string.install_keys_success,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
homeViewModel.setCheckKeys(true)
gamesViewModel.reloadGames(true) gamesViewModel.reloadGames(true)
return true return true
} else { } else {
@ -367,26 +411,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
val inputZip = contentResolver.openInputStream(result)
if (inputZip == null) {
Toast.makeText(
applicationContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
return@registerForActivityResult
}
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
val firmwarePath = val firmwarePath =
File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
val cacheFirmwareDir = File("${cacheDir.path}/registered/") val cacheFirmwareDir = File("${cacheDir.path}/registered/")
val task: () -> Any = { ProgressDialogFragment.newInstance(
this,
R.string.firmware_installing
) { progressCallback, _ ->
var messageToShow: Any var messageToShow: Any
try { try {
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) FileUtil.unzipToInternalStorage(
result.toString(),
cacheFirmwareDir,
progressCallback
)
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
@ -399,21 +440,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
firmwarePath.deleteRecursively() firmwarePath.deleteRecursively()
cacheFirmwareDir.copyRecursively(firmwarePath, true) cacheFirmwareDir.copyRecursively(firmwarePath, true)
NativeLibrary.initializeSystem(true) NativeLibrary.initializeSystem(true)
homeViewModel.setCheckKeys(true)
getString(R.string.save_file_imported_success) getString(R.string.save_file_imported_success)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.error("[MainActivity] Firmware install failed - ${e.message}")
messageToShow = getString(R.string.fatal_error) messageToShow = getString(R.string.fatal_error)
} finally { } finally {
cacheFirmwareDir.deleteRecursively() cacheFirmwareDir.deleteRecursively()
} }
messageToShow messageToShow
} }.show(supportFragmentManager, ProgressDialogFragment.TAG)
IndeterminateProgressDialogFragment.newInstance(
this,
R.string.firmware_installing,
task = task
).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
val getAmiiboKey = val getAmiiboKey =
@ -472,11 +509,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
this@MainActivity, this@MainActivity,
R.string.verifying_content, R.string.verifying_content,
false false
) { ) { _, _ ->
var updatesMatchProgram = true var updatesMatchProgram = true
for (document in documents) { for (document in documents) {
val valid = NativeLibrary.doesUpdateMatchProgram( val valid = NativeLibrary.doesUpdateMatchProgram(
@ -499,44 +536,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
positiveAction = { homeViewModel.setContentToInstall(documents) } positiveAction = { homeViewModel.setContentToInstall(documents) }
) )
} }
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, ProgressDialogFragment.TAG)
} }
private fun installContent(documents: List<Uri>) { private fun installContent(documents: List<Uri>) {
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
this@MainActivity, this@MainActivity,
R.string.installing_game_content R.string.installing_game_content
) { ) { progressCallback, messageCallback ->
var installSuccess = 0 var installSuccess = 0
var installOverwrite = 0 var installOverwrite = 0
var errorBaseGame = 0 var errorBaseGame = 0
var errorExtension = 0 var error = 0
var errorOther = 0
documents.forEach { documents.forEach {
messageCallback.invoke(FileUtil.getFilename(it))
when ( when (
NativeLibrary.installFileToNand( InstallResult.from(
it.toString(), NativeLibrary.installFileToNand(
FileUtil.getExtension(it) it.toString(),
progressCallback
)
) )
) { ) {
NativeLibrary.InstallFileToNandResult.Success -> { InstallResult.Success -> {
installSuccess += 1 installSuccess += 1
} }
NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { InstallResult.Overwrite -> {
installOverwrite += 1 installOverwrite += 1
} }
NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { InstallResult.BaseInstallAttempted -> {
errorBaseGame += 1 errorBaseGame += 1
} }
NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { InstallResult.Failure -> {
errorExtension += 1 error += 1
}
else -> {
errorOther += 1
} }
} }
} }
@ -563,7 +598,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
) )
installResult.append(separator) installResult.append(separator)
} }
val errorTotal: Int = errorBaseGame + errorExtension + errorOther val errorTotal: Int = errorBaseGame + error
if (errorTotal > 0) { if (errorTotal > 0) {
installResult.append(separator) installResult.append(separator)
installResult.append( installResult.append(
@ -580,14 +615,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
) )
installResult.append(separator) installResult.append(separator)
} }
if (errorExtension > 0) { if (error > 0) {
installResult.append(separator)
installResult.append(
getString(R.string.install_game_content_failure_file_extension)
)
installResult.append(separator)
}
if (errorOther > 0) {
installResult.append( installResult.append(
getString(R.string.install_game_content_failure_description) getString(R.string.install_game_content_failure_description)
) )
@ -606,7 +634,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
descriptionString = installResult.toString().trim() descriptionString = installResult.toString().trim()
) )
} }
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, ProgressDialogFragment.TAG)
} }
val exportUserData = registerForActivityResult( val exportUserData = registerForActivityResult(
@ -616,16 +644,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
this, this,
R.string.exporting_user_data, R.string.exporting_user_data,
true true
) { ) { progressCallback, _ ->
val zipResult = FileUtil.zipFromInternalStorage( val zipResult = FileUtil.zipFromInternalStorage(
File(DirectoryInitialization.userDirectory!!), File(DirectoryInitialization.userDirectory!!),
DirectoryInitialization.userDirectory!!, DirectoryInitialization.userDirectory!!,
BufferedOutputStream(contentResolver.openOutputStream(result)), BufferedOutputStream(contentResolver.openOutputStream(result)),
taskViewModel.cancelled, progressCallback,
compression = false compression = false
) )
return@newInstance when (zipResult) { return@newInstance when (zipResult) {
@ -633,7 +661,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
TaskState.Failed -> R.string.export_failed TaskState.Failed -> R.string.export_failed
TaskState.Cancelled -> R.string.user_data_export_cancelled TaskState.Cancelled -> R.string.user_data_export_cancelled
} }
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, ProgressDialogFragment.TAG)
} }
val importUserData = val importUserData =
@ -642,10 +670,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
IndeterminateProgressDialogFragment.newInstance( ProgressDialogFragment.newInstance(
this, this,
R.string.importing_user_data R.string.importing_user_data
) { ) { progressCallback, _ ->
val checkStream = val checkStream =
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
var isYuzuBackup = false var isYuzuBackup = false
@ -674,8 +702,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
// Copy archive to internal storage // Copy archive to internal storage
try { try {
FileUtil.unzipToInternalStorage( FileUtil.unzipToInternalStorage(
BufferedInputStream(contentResolver.openInputStream(result)), result.toString(),
File(DirectoryInitialization.userDirectory!!) File(DirectoryInitialization.userDirectory!!),
progressCallback
) )
} catch (e: Exception) { } catch (e: Exception) {
return@newInstance MessageDialogFragment.newInstance( return@newInstance MessageDialogFragment.newInstance(
@ -689,8 +718,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
NativeLibrary.initializeSystem(true) NativeLibrary.initializeSystem(true)
NativeConfig.initializeGlobalConfig() NativeConfig.initializeGlobalConfig()
gamesViewModel.reloadGames(false) gamesViewModel.reloadGames(false)
driverViewModel.reloadDriverData()
return@newInstance getString(R.string.user_data_import_success) return@newInstance getString(R.string.user_data_import_success)
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) }.show(supportFragmentManager, ProgressDialogFragment.TAG)
} }
} }

View File

@ -7,7 +7,6 @@ import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.flow.StateFlow
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -19,6 +18,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.MinimalDocumentFile import org.yuzu.yuzu_emu.model.MinimalDocumentFile
import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskState
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.OutputStream
import java.lang.NullPointerException import java.lang.NullPointerException
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.zip.Deflater import java.util.zip.Deflater
@ -104,7 +104,7 @@ object FileUtil {
/** /**
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow * Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
* This function will be faster than DoucmentFile.listFiles * This function will be faster than DocumentFile.listFiles
* @param uri Directory uri. * @param uri Directory uri.
* @return CheapDocument lists. * @return CheapDocument lists.
*/ */
@ -283,12 +283,34 @@ object FileUtil {
/** /**
* Extracts the given zip file into the given directory. * Extracts the given zip file into the given directory.
* @param path String representation of a [Uri] or a typical path delimited by '/'
* @param destDir Location to unzip the contents of [path] into
* @param progressCallback Lambda that is called with the total number of files and the current
* progress through the process. Stops execution as soon as possible if this returns true.
*/ */
@Throws(SecurityException::class) @Throws(SecurityException::class)
fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { fun unzipToInternalStorage(
ZipInputStream(zipStream).use { zis -> path: String,
destDir: File,
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
) {
var totalEntries = 0L
ZipInputStream(getInputStream(path)).use { zis ->
var tempEntry = zis.nextEntry
while (tempEntry != null) {
tempEntry = zis.nextEntry
totalEntries++
}
}
var progress = 0L
ZipInputStream(getInputStream(path)).use { zis ->
var entry: ZipEntry? = zis.nextEntry var entry: ZipEntry? = zis.nextEntry
while (entry != null) { while (entry != null) {
if (progressCallback.invoke(totalEntries, progress)) {
return@use
}
val newFile = File(destDir, entry.name) val newFile = File(destDir, entry.name)
val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
@ -304,6 +326,7 @@ object FileUtil {
newFile.outputStream().use { fos -> zis.copyTo(fos) } newFile.outputStream().use { fos -> zis.copyTo(fos) }
} }
entry = zis.nextEntry entry = zis.nextEntry
progress++
} }
} }
} }
@ -313,14 +336,15 @@ object FileUtil {
* @param inputFile File representation of the item that will be zipped * @param inputFile File representation of the item that will be zipped
* @param rootDir Directory containing the inputFile * @param rootDir Directory containing the inputFile
* @param outputStream Stream where the zip file will be output * @param outputStream Stream where the zip file will be output
* @param cancelled [StateFlow] that reports whether this process has been cancelled * @param progressCallback Lambda that is called with the total number of files and the current
* progress through the process. Stops execution as soon as possible if this returns true.
* @param compression Disables compression if true * @param compression Disables compression if true
*/ */
fun zipFromInternalStorage( fun zipFromInternalStorage(
inputFile: File, inputFile: File,
rootDir: String, rootDir: String,
outputStream: BufferedOutputStream, outputStream: BufferedOutputStream,
cancelled: StateFlow<Boolean>? = null, progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false },
compression: Boolean = true compression: Boolean = true
): TaskState { ): TaskState {
try { try {
@ -330,8 +354,10 @@ object FileUtil {
zos.setLevel(Deflater.NO_COMPRESSION) zos.setLevel(Deflater.NO_COMPRESSION)
} }
var count = 0L
val totalFiles = inputFile.walkTopDown().count().toLong()
inputFile.walkTopDown().forEach { file -> inputFile.walkTopDown().forEach { file ->
if (cancelled?.value == true) { if (progressCallback.invoke(totalFiles, count)) {
return TaskState.Cancelled return TaskState.Cancelled
} }
@ -343,6 +369,7 @@ object FileUtil {
if (file.isFile) { if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) } file.inputStream().use { fis -> fis.copyTo(zos) }
} }
count++
} }
} }
} }
@ -356,9 +383,14 @@ object FileUtil {
/** /**
* Helper function that copies the contents of a DocumentFile folder into a [File] * Helper function that copies the contents of a DocumentFile folder into a [File]
* @param file [File] representation of the folder to copy into * @param file [File] representation of the folder to copy into
* @param progressCallback Lambda that is called with the total number of files and the current
* progress through the process. Stops execution as soon as possible if this returns true.
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
*/ */
fun DocumentFile.copyFilesTo(file: File) { fun DocumentFile.copyFilesTo(
file: File,
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
) {
file.mkdirs() file.mkdirs()
if (!this.isDirectory || !file.isDirectory) { if (!this.isDirectory || !file.isDirectory) {
throw IllegalStateException( throw IllegalStateException(
@ -366,7 +398,13 @@ object FileUtil {
) )
} }
var count = 0L
val totalFiles = this.listFiles().size.toLong()
this.listFiles().forEach { this.listFiles().forEach {
if (progressCallback.invoke(totalFiles, count)) {
return
}
val newFile = File(file, it.name!!) val newFile = File(file, it.name!!)
if (it.isDirectory) { if (it.isDirectory) {
newFile.mkdirs() newFile.mkdirs()
@ -381,6 +419,7 @@ object FileUtil {
newFile.outputStream().use { os -> bos.copyTo(os) } newFile.outputStream().use { os -> bos.copyTo(os) }
} }
} }
count++
} }
} }
@ -427,6 +466,18 @@ object FileUtil {
} }
} }
fun getInputStream(path: String) = if (path.contains("content://")) {
Uri.parse(path).inputStream()
} else {
File(path).inputStream()
}
fun getOutputStream(path: String) = if (path.contains("content://")) {
Uri.parse(path).outputStream()
} else {
File(path).outputStream()
}
@Throws(IOException::class) @Throws(IOException::class)
fun getStringFromFile(file: File): String = fun getStringFromFile(file: File): String =
String(file.readBytes(), StandardCharsets.UTF_8) String(file.readBytes(), StandardCharsets.UTF_8)
@ -434,4 +485,19 @@ object FileUtil {
@Throws(IOException::class) @Throws(IOException::class)
fun getStringFromInputStream(stream: InputStream): String = fun getStringFromInputStream(stream: InputStream): String =
String(stream.readBytes(), StandardCharsets.UTF_8) String(stream.readBytes(), StandardCharsets.UTF_8)
fun DocumentFile.inputStream(): InputStream =
YuzuApplication.appContext.contentResolver.openInputStream(uri)!!
fun DocumentFile.outputStream(): OutputStream =
YuzuApplication.appContext.contentResolver.openOutputStream(uri)!!
fun Uri.inputStream(): InputStream =
YuzuApplication.appContext.contentResolver.openInputStream(this)!!
fun Uri.outputStream(): OutputStream =
YuzuApplication.appContext.contentResolver.openOutputStream(this)!!
fun Uri.asDocumentFile(): DocumentFile? =
DocumentFile.fromSingleUri(YuzuApplication.appContext, this)
} }

View File

@ -5,7 +5,10 @@ package org.yuzu.yuzu_emu.utils
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.LayerDrawable
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@ -85,4 +88,22 @@ object GameIconUtils {
return imageLoader.execute(request) return imageLoader.execute(request)
.drawable!!.toBitmap(config = Bitmap.Config.ARGB_8888) .drawable!!.toBitmap(config = Bitmap.Config.ARGB_8888)
} }
suspend fun getShortcutIcon(lifecycleOwner: LifecycleOwner, game: Game): IconCompat {
val layerDrawable = ResourcesCompat.getDrawable(
YuzuApplication.appContext.resources,
R.drawable.shortcut,
null
) as LayerDrawable
layerDrawable.setDrawableByLayerId(
R.id.shortcut_foreground,
getGameIcon(lifecycleOwner, game).toDrawable(YuzuApplication.appContext.resources)
)
val inset = YuzuApplication.appContext.resources
.getDimensionPixelSize(R.dimen.icon_inset)
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
return IconCompat.createWithAdaptiveBitmap(
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
)
}
} }

View File

@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
@ -62,9 +61,6 @@ object GpuDriverHelper {
?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name } ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
?.distinct() ?.distinct()
?.toMutableList() ?: mutableListOf() ?.toMutableList() ?: mutableListOf()
// TODO: Get system driver information
drivers.add(0, Pair("", GpuDriverMetadata()))
return drivers return drivers
} }
@ -126,7 +122,7 @@ object GpuDriverHelper {
// Unzip the driver. // Unzip the driver.
try { try {
FileUtil.unzipToInternalStorage( FileUtil.unzipToInternalStorage(
BufferedInputStream(copiedFile.inputStream()), copiedFile.path,
File(driverInstallationPath!!) File(driverInstallationPath!!)
) )
} catch (e: SecurityException) { } catch (e: SecurityException) {
@ -159,7 +155,7 @@ object GpuDriverHelper {
// Unzip the driver to the private installation directory // Unzip the driver to the private installation directory
try { try {
FileUtil.unzipToInternalStorage( FileUtil.unzipToInternalStorage(
BufferedInputStream(driver.inputStream()), driver.path,
File(driverInstallationPath!!) File(driverInstallationPath!!)
) )
} catch (e: SecurityException) { } catch (e: SecurityException) {

View File

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.viewholder
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.yuzu.yuzu_emu.adapters.AbstractDiffAdapter
import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
/**
* [RecyclerView.ViewHolder] meant to work together with a [AbstractDiffAdapter] or a
* [AbstractListAdapter] so we can run [bind] on each list item without needing a manual hookup.
*/
abstract class AbstractViewHolder<Model>(binding: ViewBinding) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(model: Model)
}

View File

@ -42,3 +42,19 @@ double GetJDouble(JNIEnv* env, jobject jdouble) {
jobject ToJDouble(JNIEnv* env, double value) { jobject ToJDouble(JNIEnv* env, double value) {
return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value); return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
} }
s32 GetJInteger(JNIEnv* env, jobject jinteger) {
return env->GetIntField(jinteger, IDCache::GetIntegerValueField());
}
jobject ToJInteger(JNIEnv* env, s32 value) {
return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value);
}
bool GetJBoolean(JNIEnv* env, jobject jboolean) {
return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField());
}
jobject ToJBoolean(JNIEnv* env, bool value) {
return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value);
}

View File

@ -6,6 +6,7 @@
#include <string> #include <string>
#include <jni.h> #include <jni.h>
#include "common/common_types.h"
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);
@ -13,3 +14,9 @@ jstring ToJString(JNIEnv* env, std::u16string_view str);
double GetJDouble(JNIEnv* env, jobject jdouble); double GetJDouble(JNIEnv* env, jobject jdouble);
jobject ToJDouble(JNIEnv* env, double value); jobject ToJDouble(JNIEnv* env, double value);
s32 GetJInteger(JNIEnv* env, jobject jinteger);
jobject ToJInteger(JNIEnv* env, s32 value);
bool GetJBoolean(JNIEnv* env, jobject jboolean);
jobject ToJBoolean(JNIEnv* env, bool value);

View File

@ -21,7 +21,7 @@ void AndroidConfig::ReloadAllValues() {
} }
void AndroidConfig::SaveAllValues() { void AndroidConfig::SaveAllValues() {
Save(); SaveValues();
SaveAndroidValues(); SaveAndroidValues();
} }

View File

@ -63,6 +63,7 @@ struct Values {
Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay", Settings::Setting<bool> show_input_overlay{linkage, true, "show_input_overlay",
Settings::Category::Overlay}; Settings::Category::Overlay};
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay}; Settings::Setting<bool> touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay};
Settings::Setting<s32> lock_drawer{linkage, false, "lock_drawer", Settings::Category::Overlay};
}; };
extern Values values; extern Values values;

View File

@ -82,7 +82,7 @@ AndroidKeyboard::ResultData AndroidKeyboard::ResultData::CreateFromFrontend(jobj
const jstring string = reinterpret_cast<jstring>(env->GetObjectField( const jstring string = reinterpret_cast<jstring>(env->GetObjectField(
object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;"))); object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;")));
return ResultData{GetJString(env, string), return ResultData{GetJString(env, string),
static_cast<Service::AM::Applets::SwkbdResult>(env->GetIntField( static_cast<Service::AM::Frontend::SwkbdResult>(env->GetIntField(
object, env->GetFieldID(s_keyboard_data_class, "result", "I")))}; object, env->GetFieldID(s_keyboard_data_class, "result", "I")))};
} }
@ -149,7 +149,7 @@ void AndroidKeyboard::ShowNormalKeyboard() const {
} }
void AndroidKeyboard::ShowTextCheckDialog( void AndroidKeyboard::ShowTextCheckDialog(
Service::AM::Applets::SwkbdTextCheckResult text_check_result, Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const { std::u16string text_check_message) const {
LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog."); LOG_WARNING(Frontend, "(STUBBED) called, backend requested to show the text check dialog.");
} }
@ -204,7 +204,7 @@ void AndroidKeyboard::InlineTextChanged(
"\ncursor_position={}", "\ncursor_position={}",
Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position); Common::UTF16ToUTF8(text_parameters.input_text), text_parameters.cursor_position);
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString,
text_parameters.input_text, text_parameters.cursor_position); text_parameters.input_text, text_parameters.cursor_position);
} }
@ -219,7 +219,7 @@ void AndroidKeyboard::SubmitInlineKeyboardText(std::u16string submitted_text) {
m_current_text += submitted_text; m_current_text += submitted_text;
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text, submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text,
m_current_text.size()); m_current_text.size());
} }
@ -236,12 +236,12 @@ void AndroidKeyboard::SubmitInlineKeyboardInput(int key_code) {
case KEYCODE_BACK: case KEYCODE_BACK:
case KEYCODE_ENTER: case KEYCODE_ENTER:
m_is_inline_active = false; m_is_inline_active = false;
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::DecidedEnter, m_current_text, submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::DecidedEnter, m_current_text,
static_cast<s32>(m_current_text.size())); static_cast<s32>(m_current_text.size()));
break; break;
case KEYCODE_DEL: case KEYCODE_DEL:
m_current_text.pop_back(); m_current_text.pop_back();
submit_inline_callback(Service::AM::Applets::SwkbdReplyType::ChangedString, m_current_text, submit_inline_callback(Service::AM::Frontend::SwkbdReplyType::ChangedString, m_current_text,
m_current_text.size()); m_current_text.size());
break; break;
} }

View File

@ -24,7 +24,7 @@ public:
void ShowNormalKeyboard() const override; void ShowNormalKeyboard() const override;
void ShowTextCheckDialog(Service::AM::Applets::SwkbdTextCheckResult text_check_result, void ShowTextCheckDialog(Service::AM::Frontend::SwkbdTextCheckResult text_check_result,
std::u16string text_check_message) const override; std::u16string text_check_message) const override;
void ShowInlineKeyboard( void ShowInlineKeyboard(
@ -45,7 +45,7 @@ private:
static ResultData CreateFromFrontend(jobject object); static ResultData CreateFromFrontend(jobject object);
std::string text; std::string text;
Service::AM::Applets::SwkbdResult result{}; Service::AM::Frontend::SwkbdResult result{};
}; };
void SubmitNormalText(const ResultData& result) const; void SubmitNormalText(const ResultData& result) const;

View File

@ -1,12 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <core/core.h> #include "core/core.h"
#include <core/file_sys/mode.h> #include "core/file_sys/fs_filesystem.h"
#include <core/file_sys/patch_manager.h> #include "core/file_sys/patch_manager.h"
#include <core/loader/nro.h>
#include <jni.h>
#include "core/loader/loader.h" #include "core/loader/loader.h"
#include "core/loader/nro.h"
#include "jni.h"
#include "jni/android_common/android_common.h" #include "jni/android_common/android_common.h"
#include "native.h" #include "native.h"
@ -79,7 +79,7 @@ extern "C" {
jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobject obj, jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobject obj,
jstring jpath) { jstring jpath) {
const auto file = EmulationSession::GetInstance().System().GetFilesystem()->OpenFile( const auto file = EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
GetJString(env, jpath), FileSys::Mode::Read); GetJString(env, jpath), FileSys::OpenMode::Read);
if (!file) { if (!file) {
return false; return false;
} }

View File

@ -43,10 +43,27 @@ static jfieldID s_overlay_control_data_landscape_position_field;
static jfieldID s_overlay_control_data_portrait_position_field; static jfieldID s_overlay_control_data_portrait_position_field;
static jfieldID s_overlay_control_data_foldable_position_field; static jfieldID s_overlay_control_data_foldable_position_field;
static jclass s_patch_class;
static jmethodID s_patch_constructor;
static jfieldID s_patch_enabled_field;
static jfieldID s_patch_name_field;
static jfieldID s_patch_version_field;
static jfieldID s_patch_type_field;
static jfieldID s_patch_program_id_field;
static jfieldID s_patch_title_id_field;
static jclass s_double_class; static jclass s_double_class;
static jmethodID s_double_constructor; static jmethodID s_double_constructor;
static jfieldID s_double_value_field; static jfieldID s_double_value_field;
static jclass s_integer_class;
static jmethodID s_integer_constructor;
static jfieldID s_integer_value_field;
static jclass s_boolean_class;
static jmethodID s_boolean_constructor;
static jfieldID s_boolean_value_field;
static constexpr jint JNI_VERSION = JNI_VERSION_1_6; static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
namespace IDCache { namespace IDCache {
@ -186,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() {
return s_overlay_control_data_foldable_position_field; return s_overlay_control_data_foldable_position_field;
} }
jclass GetPatchClass() {
return s_patch_class;
}
jmethodID GetPatchConstructor() {
return s_patch_constructor;
}
jfieldID GetPatchEnabledField() {
return s_patch_enabled_field;
}
jfieldID GetPatchNameField() {
return s_patch_name_field;
}
jfieldID GetPatchVersionField() {
return s_patch_version_field;
}
jfieldID GetPatchTypeField() {
return s_patch_type_field;
}
jfieldID GetPatchProgramIdField() {
return s_patch_program_id_field;
}
jfieldID GetPatchTitleIdField() {
return s_patch_title_id_field;
}
jclass GetDoubleClass() { jclass GetDoubleClass() {
return s_double_class; return s_double_class;
} }
@ -198,6 +247,30 @@ jfieldID GetDoubleValueField() {
return s_double_value_field; return s_double_value_field;
} }
jclass GetIntegerClass() {
return s_integer_class;
}
jmethodID GetIntegerConstructor() {
return s_integer_constructor;
}
jfieldID GetIntegerValueField() {
return s_integer_value_field;
}
jclass GetBooleanClass() {
return s_boolean_class;
}
jmethodID GetBooleanConstructor() {
return s_boolean_constructor;
}
jfieldID GetBooleanValueField() {
return s_boolean_value_field;
}
} // namespace IDCache } // namespace IDCache
#ifdef __cplusplus #ifdef __cplusplus
@ -278,12 +351,37 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;"); env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;");
env->DeleteLocalRef(overlay_control_data_class); env->DeleteLocalRef(overlay_control_data_class);
const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch");
s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
s_patch_constructor = env->GetMethodID(
patch_class, "<init>",
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V");
s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
s_patch_type_field = env->GetFieldID(patch_class, "type", "I");
s_patch_program_id_field = env->GetFieldID(patch_class, "programId", "Ljava/lang/String;");
s_patch_title_id_field = env->GetFieldID(patch_class, "titleId", "Ljava/lang/String;");
env->DeleteLocalRef(patch_class);
const jclass double_class = env->FindClass("java/lang/Double"); const jclass double_class = env->FindClass("java/lang/Double");
s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class)); s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class));
s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V"); s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V");
s_double_value_field = env->GetFieldID(double_class, "value", "D"); s_double_value_field = env->GetFieldID(double_class, "value", "D");
env->DeleteLocalRef(double_class); env->DeleteLocalRef(double_class);
const jclass int_class = env->FindClass("java/lang/Integer");
s_integer_class = reinterpret_cast<jclass>(env->NewGlobalRef(int_class));
s_integer_constructor = env->GetMethodID(int_class, "<init>", "(I)V");
s_integer_value_field = env->GetFieldID(int_class, "value", "I");
env->DeleteLocalRef(int_class);
const jclass boolean_class = env->FindClass("java/lang/Boolean");
s_boolean_class = reinterpret_cast<jclass>(env->NewGlobalRef(boolean_class));
s_boolean_constructor = env->GetMethodID(boolean_class, "<init>", "(Z)V");
s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
env->DeleteLocalRef(boolean_class);
// Initialize Android Storage // Initialize Android Storage
Common::FS::Android::RegisterCallbacks(env, s_native_library_class); Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
@ -309,7 +407,10 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
env->DeleteGlobalRef(s_string_class); env->DeleteGlobalRef(s_string_class);
env->DeleteGlobalRef(s_pair_class); env->DeleteGlobalRef(s_pair_class);
env->DeleteGlobalRef(s_overlay_control_data_class); env->DeleteGlobalRef(s_overlay_control_data_class);
env->DeleteGlobalRef(s_patch_class);
env->DeleteGlobalRef(s_double_class); env->DeleteGlobalRef(s_double_class);
env->DeleteGlobalRef(s_integer_class);
env->DeleteGlobalRef(s_boolean_class);
// UnInitialize applets // UnInitialize applets
SoftwareKeyboard::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env);

View File

@ -43,8 +43,25 @@ jfieldID GetOverlayControlDataLandscapePositionField();
jfieldID GetOverlayControlDataPortraitPositionField(); jfieldID GetOverlayControlDataPortraitPositionField();
jfieldID GetOverlayControlDataFoldablePositionField(); jfieldID GetOverlayControlDataFoldablePositionField();
jclass GetPatchClass();
jmethodID GetPatchConstructor();
jfieldID GetPatchEnabledField();
jfieldID GetPatchNameField();
jfieldID GetPatchVersionField();
jfieldID GetPatchTypeField();
jfieldID GetPatchProgramIdField();
jfieldID GetPatchTitleIdField();
jclass GetDoubleClass(); jclass GetDoubleClass();
jmethodID GetDoubleConstructor(); jmethodID GetDoubleConstructor();
jfieldID GetDoubleValueField(); jfieldID GetDoubleValueField();
jclass GetIntegerClass();
jmethodID GetIntegerConstructor();
jfieldID GetIntegerValueField();
jclass GetBooleanClass();
jmethodID GetBooleanConstructor();
jfieldID GetBooleanValueField();
} // namespace IDCache } // namespace IDCache

View File

@ -17,6 +17,7 @@
#include <core/file_sys/patch_manager.h> #include <core/file_sys/patch_manager.h>
#include <core/file_sys/savedata_factory.h> #include <core/file_sys/savedata_factory.h>
#include <core/loader/nro.h> #include <core/loader/nro.h>
#include <frontend_common/content_manager.h>
#include <jni.h> #include <jni.h>
#include "common/detached_tasks.h" #include "common/detached_tasks.h"
@ -34,20 +35,22 @@
#include "core/crypto/key_manager.h" #include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h" #include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h" #include "core/file_sys/content_archive.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/submission_package.h" #include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs.h" #include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs_real.h" #include "core/file_sys/vfs/vfs_real.h"
#include "core/frontend/applets/cabinet.h" #include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h" #include "core/frontend/applets/controller.h"
#include "core/frontend/applets/error.h" #include "core/frontend/applets/error.h"
#include "core/frontend/applets/general_frontend.h" #include "core/frontend/applets/general.h"
#include "core/frontend/applets/mii_edit.h" #include "core/frontend/applets/mii_edit.h"
#include "core/frontend/applets/profile_select.h" #include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h" #include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h" #include "core/frontend/applets/web_browser.h"
#include "core/hle/service/am/applet_ae.h" #include "core/hle/service/am/applet_ae.h"
#include "core/hle/service/am/applet_manager.h"
#include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applet_oe.h"
#include "core/hle/service/am/applets/applets.h" #include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h" #include "core/loader/loader.h"
#include "frontend_common/config.h" #include "frontend_common/config.h"
@ -100,67 +103,6 @@ void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
m_native_window = native_window; m_native_window = native_window;
} }
int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) {
jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
std::size_t block_size) {
if (src == nullptr || dest == nullptr) {
return false;
}
if (!dest->Resize(src->GetSize())) {
return false;
}
using namespace Common::Literals;
[[maybe_unused]] std::vector<u8> buffer(1_MiB);
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
jconst read = src->Read(buffer.data(), buffer.size(), i);
dest->Write(buffer.data(), read, i);
}
return true;
};
enum InstallResult {
Success = 0,
SuccessFileOverwritten = 1,
InstallError = 2,
ErrorBaseGame = 3,
ErrorFilenameExtension = 4,
};
[[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
if (file_extension == "nsp") {
nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
if (nsp->IsExtractedType()) {
return InstallError;
}
} else {
return ErrorFilenameExtension;
}
if (!nsp) {
return InstallError;
}
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
return InstallError;
}
jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true,
copy_func);
switch (res) {
case FileSys::InstallResult::Success:
return Success;
case FileSys::InstallResult::OverwriteExisting:
return SuccessFileOverwritten;
case FileSys::InstallResult::ErrorBaseInstall:
return ErrorBaseGame;
default:
return InstallError;
}
}
void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir, void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
const std::string& custom_driver_dir, const std::string& custom_driver_dir,
const std::string& custom_driver_name, const std::string& custom_driver_name,
@ -214,7 +156,7 @@ void EmulationSession::SurfaceChanged() {
} }
void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) { void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) {
const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::Mode::Read); const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::OpenMode::Read);
if (!file) { if (!file) {
return; return;
} }
@ -267,6 +209,12 @@ void EmulationSession::InitializeSystem(bool reload) {
m_system.GetFileSystemController().CreateFactories(*m_vfs); m_system.GetFileSystemController().CreateFactories(*m_vfs);
} }
void EmulationSession::SetAppletId(int applet_id) {
m_applet_id = applet_id;
m_system.GetFrontendAppletHolder().SetCurrentAppletId(
static_cast<Service::AM::AppletId>(m_applet_id));
}
Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath) { Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath) {
std::scoped_lock lock(m_mutex); std::scoped_lock lock(m_mutex);
@ -281,7 +229,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
m_system.ApplySettings(); m_system.ApplySettings();
Settings::LogSettings(); Settings::LogSettings();
m_system.HIDCore().ReloadInputDevices(); m_system.HIDCore().ReloadInputDevices();
m_system.SetAppletFrontendSet({ m_system.SetFrontendAppletSet({
nullptr, // Amiibo Settings nullptr, // Amiibo Settings
nullptr, // Controller Selector nullptr, // Controller Selector
nullptr, // Error Display nullptr, // Error Display
@ -297,7 +245,10 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
ConfigureFilesystemProvider(filepath); ConfigureFilesystemProvider(filepath);
// Load the ROM. // Load the ROM.
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath); Service::AM::FrontendAppletParameters params{
.applet_id = static_cast<Service::AM::AppletId>(m_applet_id),
};
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params);
if (m_load_result != Core::SystemResultStatus::Success) { if (m_load_result != Core::SystemResultStatus::Success) {
return m_load_result; return m_load_result;
} }
@ -307,6 +258,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
m_system.GetCpuManager().OnGpuReady(); m_system.GetCpuManager().OnGpuReady();
m_system.RegisterExitCallback([&] { HaltEmulation(); }); m_system.RegisterExitCallback([&] { HaltEmulation(); });
OnEmulationStarted();
return Core::SystemResultStatus::Success; return Core::SystemResultStatus::Success;
} }
@ -381,6 +333,9 @@ void EmulationSession::RunEmulation() {
} }
} }
} }
// Reset current applet ID.
m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
} }
bool EmulationSession::IsHandheldOnly() { bool EmulationSession::IsHandheldOnly() {
@ -410,8 +365,8 @@ void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld); jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) { if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController); handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::ProController); controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
handheld->Disconnect(); handheld->Disconnect();
} }
} }
@ -512,10 +467,20 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject
} }
int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
jstring j_file, jstring j_file, jobject jcallback) {
jstring j_file_extension) { auto jlambdaClass = env->GetObjectClass(jcallback);
return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file), auto jlambdaInvokeMethod = env->GetMethodID(
GetJString(env, j_file_extension)); jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
ToJDouble(env, max), ToJDouble(env, progress));
return GetJBoolean(env, jwasCancelled);
};
return static_cast<int>(
ContentManager::InstallNSP(EmulationSession::GetInstance().System(),
*EmulationSession::GetInstance().System().GetFilesystem(),
GetJString(env, j_file), callback));
} }
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj, jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
@ -524,8 +489,8 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* en
u64 program_id = EmulationSession::GetProgramId(env, jprogramId); u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
std::string updatePath = GetJString(env, jupdatePath); std::string updatePath = GetJString(env, jupdatePath);
std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>( std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>(
EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath, EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
FileSys::Mode::Read)); updatePath, FileSys::OpenMode::Read));
for (const auto& item : nsp->GetNCAs()) { for (const auto& item : nsp->GetNCAs()) {
for (const auto& nca_details : item.second) { for (const auto& nca_details : item.second) {
if (nca_details.second->GetName().ends_with(".cnmt.nca")) { if (nca_details.second->GetName().ends_with(".cnmt.nca")) {
@ -724,6 +689,11 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass
return ToJString(env, "JIT"); return ToJString(env, "JIT");
} }
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject jobj) {
return ToJString(env,
EmulationSession::GetInstance().System().GPU().Renderer().GetDeviceVendor());
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
EmulationSession::GetInstance().System().ApplySettings(); EmulationSession::GetInstance().System().ApplySettings();
} }
@ -763,15 +733,15 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv*
jobject instance) { jobject instance) {
const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory( auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory(
Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read); Common::FS::PathToUTF8String(nand_dir), FileSys::OpenMode::Read);
const auto user_id = EmulationSession::GetInstance().System().GetProfileManager().GetUser( const auto user_id = EmulationSession::GetInstance().System().GetProfileManager().GetUser(
static_cast<std::size_t>(0)); static_cast<std::size_t>(0));
ASSERT(user_id); ASSERT(user_id);
const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, {}, vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, 1,
FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0); user_id->AsU128(), 0);
const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path);
if (!Common::FS::CreateParentDirs(full_path)) { if (!Common::FS::CreateParentDirs(full_path)) {
@ -798,13 +768,12 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, j
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentAppletId(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentAppletId(JNIEnv* env, jclass clazz,
jint jappletId) { jint jappletId) {
EmulationSession::GetInstance().System().GetAppletManager().SetCurrentAppletId( EmulationSession::GetInstance().SetAppletId(jappletId);
static_cast<Service::AM::Applets::AppletId>(jappletId));
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclass clazz,
jint jcabinetMode) { jint jcabinetMode) {
EmulationSession::GetInstance().System().GetAppletManager().SetCabinetMode( EmulationSession::GetInstance().System().GetFrontendAppletHolder().SetCabinetMode(
static_cast<Service::NFP::CabinetMode>(jcabinetMode)); static_cast<Service::NFP::CabinetMode>(jcabinetMode));
} }
@ -824,9 +793,9 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
return true; return true;
} }
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj, jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
jstring jpath, jstring jpath,
jstring jprogramId) { jstring jprogramId) {
const auto path = GetJString(env, jpath); const auto path = GetJString(env, jpath);
const auto vFile = const auto vFile =
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
@ -843,20 +812,78 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env,
FileSys::VirtualFile update_raw; FileSys::VirtualFile update_raw;
loader->ReadUpdateRaw(update_raw); loader->ReadUpdateRaw(update_raw);
auto addons = pm.GetPatchVersionNames(update_raw); auto patches = pm.GetPatches(update_raw);
auto jemptyString = ToJString(env, ""); jobjectArray jpatchArray =
auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr);
jemptyString, jemptyString);
jobjectArray jaddonsArray =
env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
int i = 0; int i = 0;
for (const auto& addon : addons) { for (const auto& patch : patches) {
jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), jobject jpatch = env->NewObject(
ToJString(env, addon.first), ToJString(env, addon.second)); IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled,
env->SetObjectArrayElement(jaddonsArray, i, jaddon); ToJString(env, patch.name), ToJString(env, patch.version),
static_cast<jint>(patch.type), ToJString(env, std::to_string(patch.program_id)),
ToJString(env, std::to_string(patch.title_id)));
env->SetObjectArrayElement(jpatchArray, i, jpatch);
++i; ++i;
} }
return jaddonsArray; return jpatchArray;
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj,
jstring jprogramId) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(),
program_id);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj,
jstring jprogramId) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
ContentManager::RemoveAllDLC(EmulationSession::GetInstance().System(), program_id);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId,
jstring jname) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(),
program_id, GetJString(env, jname));
}
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyInstalledContents(JNIEnv* env,
jobject jobj,
jobject jcallback) {
auto jlambdaClass = env->GetObjectClass(jcallback);
auto jlambdaInvokeMethod = env->GetMethodID(
jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
ToJDouble(env, max), ToJDouble(env, progress));
return GetJBoolean(env, jwasCancelled);
};
auto& session = EmulationSession::GetInstance();
std::vector<std::string> result = ContentManager::VerifyInstalledContents(
session.System(), *session.GetContentProvider(), callback);
jobjectArray jresult =
env->NewObjectArray(result.size(), IDCache::GetStringClass(), ToJString(env, ""));
for (size_t i = 0; i < result.size(); ++i) {
env->SetObjectArrayElement(jresult, i, ToJString(env, result[i]));
}
return jresult;
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyGameContents(JNIEnv* env, jobject jobj,
jstring jpath, jobject jcallback) {
auto jlambdaClass = env->GetObjectClass(jcallback);
auto jlambdaInvokeMethod = env->GetMethodID(
jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
ToJDouble(env, max), ToJDouble(env, progress));
return GetJBoolean(env, jwasCancelled);
};
auto& session = EmulationSession::GetInstance();
return static_cast<jint>(
ContentManager::VerifyGameContents(session.System(), GetJString(env, jpath), callback));
} }
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
@ -875,10 +902,10 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j
const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir), auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir),
FileSys::Mode::Read); FileSys::OpenMode::Read);
const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, {}, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
program_id, user_id->AsU128(), 0); program_id, user_id->AsU128(), 0);
return ToJString(env, user_save_data_path); return ToJString(env, user_save_data_path);
} }
@ -905,4 +932,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env,
EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries(); EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
} }
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobject jobj) {
auto& system = EmulationSession::GetInstance().System();
system.GetFileSystemController().CreateFactories(*system.GetFilesystem());
return ContentManager::AreKeysPresent();
}
} // extern "C" } // extern "C"

View File

@ -7,6 +7,7 @@
#include "core/file_sys/registered_cache.h" #include "core/file_sys/registered_cache.h"
#include "core/hle/service/acc/profile_manager.h" #include "core/hle/service/acc/profile_manager.h"
#include "core/perf_stats.h" #include "core/perf_stats.h"
#include "frontend_common/content_manager.h"
#include "jni/applets/software_keyboard.h" #include "jni/applets/software_keyboard.h"
#include "jni/emu_window/emu_window.h" #include "jni/emu_window/emu_window.h"
#include "video_core/rasterizer_interface.h" #include "video_core/rasterizer_interface.h"
@ -29,7 +30,6 @@ public:
void SetNativeWindow(ANativeWindow* native_window); void SetNativeWindow(ANativeWindow* native_window);
void SurfaceChanged(); void SurfaceChanged();
int InstallFileToNand(std::string filename, std::string file_extension);
void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
const std::string& custom_driver_name, const std::string& custom_driver_name,
const std::string& file_redirect_dir); const std::string& file_redirect_dir);
@ -45,6 +45,7 @@ public:
const Core::PerfStatsResults& PerfStats(); const Core::PerfStatsResults& PerfStats();
void ConfigureFilesystemProvider(const std::string& filepath); void ConfigureFilesystemProvider(const std::string& filepath);
void InitializeSystem(bool reload); void InitializeSystem(bool reload);
void SetAppletId(int applet_id);
Core::SystemResultStatus InitializeEmulation(const std::string& filepath); Core::SystemResultStatus InitializeEmulation(const std::string& filepath);
bool IsHandheldOnly(); bool IsHandheldOnly();
@ -77,6 +78,7 @@ private:
std::atomic<bool> m_is_paused = false; std::atomic<bool> m_is_paused = false;
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider; std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
int m_applet_id{1};
// GPU driver parameters // GPU driver parameters
std::shared_ptr<Common::DynamicLibrary> m_vulkan_library; std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;

View File

@ -205,7 +205,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEn
jstring jkey) { jstring jkey) {
auto setting = getSetting<std::string>(env, jkey); auto setting = getSetting<std::string>(env, jkey);
if (setting != nullptr) { if (setting != nullptr) {
return setting->RuntimeModfiable(); return setting->RuntimeModifiable();
} }
return true; return true;
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M280,920q-33,0 -56.5,-23.5T200,840v-720q0,-33 23.5,-56.5T280,40h400q33,0 56.5,23.5T760,120v160h-80v-40L280,240v480h400v-40h80v160q0,33 -23.5,56.5T680,920L280,920ZM686,520L480,520v120h-80v-120q0,-33 23.5,-56.5T480,440h206l-62,-64 56,-56 160,160 -160,160 -56,-56 62,-64Z" />
</vector>

View File

@ -11,12 +11,14 @@
android:id="@+id/appbar_about" android:id="@+id/appbar_about"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_about" android:id="@+id/toolbar_about"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
app:title="@string/about" /> app:title="@string/about" />
@ -28,6 +30,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:fadeScrollbars="false" android:fadeScrollbars="false"
android:scrollbars="vertical" android:scrollbars="vertical"
android:defaultFocusHighlightEnabled="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <LinearLayout
@ -147,7 +150,7 @@
android:layout_marginHorizontal="20dp" /> android:layout_marginHorizontal="20dp" />
<LinearLayout <LinearLayout
android:id="@+id/button_build_hash" android:id="@+id/button_version_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
@ -164,7 +167,7 @@
android:textAlignment="viewStart" /> android:textAlignment="viewStart" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_build_hash" android:id="@+id/text_version_name"
style="@style/TextAppearance.Material3.BodyMedium" style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_about"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_info"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:defaultFocusHighlightEnabled="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/content_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:baselineAligned="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="3"
android:gravity="top|center_horizontal"
android:paddingHorizontal="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_copy"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/copy_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_verify_integrity"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/verify_integrity" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/path_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:editable="false"
android:importantForAutofill="no"
android:inputType="none"
android:minHeight="48dp"
android:textAlignment="viewStart"
tools:text="1.0.0" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/program_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/program_id_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:editable="false"
android:importantForAutofill="no"
android:inputType="none"
android:minHeight="48dp"
android:textAlignment="viewStart"
tools:text="1.0.0" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/developer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/developer_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:editable="false"
android:importantForAutofill="no"
android:inputType="none"
android:minHeight="48dp"
android:textAlignment="viewStart"
tools:text="1.0.0" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/version_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:editable="false"
android:importantForAutofill="no"
android:inputType="none"
android:minHeight="48dp"
android:textAlignment="viewStart"
tools:text="1.0.0" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -14,6 +14,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:fadeScrollbars="false" android:fadeScrollbars="false"
android:scrollbars="vertical" android:scrollbars="vertical"
android:defaultFocusHighlightEnabled="false"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon_layout" app:layout_constraintStart_toEndOf="@+id/icon_layout"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@ -43,16 +44,35 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<Button <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/button_back" android:layout_width="match_parent"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_margin="8dp" android:layout_margin="8dp"
app:icon="@drawable/ic_back" android:orientation="horizontal">
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" /> <Button
android:id="@+id/button_back"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_back"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_shortcut"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_shortcut"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle" style="?attr/materialCardViewElevatedStyle"

View File

@ -23,6 +23,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:focusable="false"
android:clickable="false" android:clickable="false"
android:checked="false" /> android:checked="false" />

View File

@ -6,16 +6,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginVertical="12dp" android:layout_marginVertical="12dp">
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="16dp" android:padding="16dp"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical">
android:animateLayoutChanges="true">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/path" android:id="@+id/path"

View File

@ -2,16 +2,16 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
style="?attr/materialCardViewFilledStyle" style="?attr/materialCardViewElevatedStyle"
android:id="@+id/option_card" android:id="@+id/option_card"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:layout_marginHorizontal="12dp" android:layout_marginHorizontal="12dp"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:backgroundTint="?attr/colorSurfaceVariant"
android:clickable="true" android:clickable="true"
android:focusable="true"> android:focusable="true"
app:cardElevation="4dp">
<LinearLayout <LinearLayout
android:id="@+id/option_layout" android:id="@+id/option_layout"

View File

@ -1,8 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/progress_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="24dp" android:orientation="vertical">
app:trackCornerRadius="4dp" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/message"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:requiresFadingEdge="horizontal"
android:singleLine="true"
android:textAlignment="viewStart"
android:visibility="gone" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp"
app:trackCornerRadius="4dp" />
</LinearLayout>

View File

@ -11,12 +11,14 @@
android:id="@+id/appbar_about" android:id="@+id/appbar_about"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_about" android:id="@+id/toolbar_about"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:title="@string/about" app:title="@string/about"
app:navigationIcon="@drawable/ic_back" /> app:navigationIcon="@drawable/ic_back" />
@ -28,6 +30,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" android:scrollbars="vertical"
android:fadeScrollbars="false" android:fadeScrollbars="false"
android:defaultFocusHighlightEnabled="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <LinearLayout
@ -148,7 +151,7 @@
android:layout_marginHorizontal="20dp" /> android:layout_marginHorizontal="20dp" />
<LinearLayout <LinearLayout
android:id="@+id/button_build_hash" android:id="@+id/button_version_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingVertical="16dp" android:paddingVertical="16dp"
@ -165,7 +168,7 @@
android:text="@string/build" /> android:text="@string/build" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_build_hash" android:id="@+id/text_version_name"
style="@style/TextAppearance.Material3.BodyMedium" style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -11,6 +11,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
@ -19,6 +20,7 @@
android:id="@+id/toolbar_addons" android:id="@+id/toolbar_addons"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" /> app:navigationIcon="@drawable/ic_back" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
@ -28,6 +30,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false" android:clipToPadding="false"
android:defaultFocusHighlightEnabled="false"
android:nextFocusDown="@id/button_install"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -10,12 +10,14 @@
android:id="@+id/appbar_applets" android:id="@+id/appbar_applets"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_applets" android:id="@+id/toolbar_applets"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
app:title="@string/applets" /> app:title="@string/applets" />

View File

@ -15,12 +15,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
app:liftOnScrollTargetViewId="@id/list_drivers"> app:liftOnScrollTargetViewId="@id/list_drivers">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_drivers" android:id="@+id/toolbar_drivers"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
app:title="@string/gpu_driver_manager" /> app:title="@string/gpu_driver_manager" />

View File

@ -11,12 +11,14 @@
android:id="@+id/appbar_ea" android:id="@+id/appbar_ea"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_about" android:id="@+id/toolbar_about"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
app:title="@string/early_access" /> app:title="@string/early_access" />
@ -30,6 +32,7 @@
android:paddingBottom="20dp" android:paddingBottom="20dp"
android:scrollbars="vertical" android:scrollbars="vertical"
android:fadeScrollbars="false" android:fadeScrollbars="false"
android:defaultFocusHighlightEnabled="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <LinearLayout

View File

@ -5,6 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true" android:keepScreenOn="true"
android:defaultFocusHighlightEnabled="false"
tools:context="org.yuzu.yuzu_emu.fragments.EmulationFragment" tools:context="org.yuzu.yuzu_emu.fragments.EmulationFragment"
tools:openDrawer="start"> tools:openDrawer="start">
@ -24,7 +25,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" /> android:focusableInTouchMode="false"
android:defaultFocusHighlightEnabled="false" />
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/loading_indicator" android:id="@+id/loading_indicator"
@ -32,7 +34,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:focusable="false" android:defaultFocusHighlightEnabled="false"
android:clickable="false"> android:clickable="false">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -118,6 +120,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:defaultFocusHighlightEnabled="false"
android:visibility="invisible" /> android:visibility="invisible" />
<Button <Button
@ -160,6 +163,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:focusedByDefault="true"
app:headerLayout="@layout/header_in_game" app:headerLayout="@layout/header_in_game"
app:menu="@menu/menu_in_game" app:menu="@menu/menu_in_game"
tools:visibility="gone" /> tools:visibility="gone" />

View File

@ -15,12 +15,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
app:liftOnScrollTargetViewId="@id/list_folders"> app:liftOnScrollTargetViewId="@id/list_folders">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_folders" android:id="@+id/toolbar_folders"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"
app:title="@string/game_folders" /> app:title="@string/game_folders" />
@ -31,6 +33,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:defaultFocusHighlightEnabled="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -11,12 +11,14 @@
android:id="@+id/appbar_info" android:id="@+id/appbar_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:touchscreenBlocksFocus="false"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_info" android:id="@+id/toolbar_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back" /> app:navigationIcon="@drawable/ic_back" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
@ -25,6 +27,7 @@
android:id="@+id/scroll_info" android:id="@+id/scroll_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:defaultFocusHighlightEnabled="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout <LinearLayout
@ -118,6 +121,14 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/copy_details" /> android:text="@string/copy_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_verify_integrity"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/verify_integrity" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -13,7 +12,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" android:scrollbars="vertical"
android:fadeScrollbars="false" android:fadeScrollbars="false"
android:clipToPadding="false"> android:clipToPadding="false"
android:defaultFocusHighlightEnabled="false">
<LinearLayout <LinearLayout
android:id="@+id/layout_all" android:id="@+id/layout_all"
@ -22,16 +22,35 @@
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_horizontal"> android:gravity="center_horizontal">
<Button <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/button_back" android:layout_width="match_parent"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:layout_gravity="start" android:orientation="horizontal">
app:icon="@drawable/ic_back"
app:iconSize="24dp" <Button
app:iconTint="?attr/colorOnSurface" /> android:id="@+id/button_back"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_back"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_shortcut"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_shortcut"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle" style="?attr/materialCardViewElevatedStyle"
@ -45,7 +64,7 @@
android:id="@+id/image_game_screen" android:id="@+id/image_game_screen"
android:layout_width="175dp" android:layout_width="175dp"
android:layout_height="175dp" android:layout_height="175dp"
tools:src="@drawable/default_icon"/> tools:src="@drawable/default_icon" />
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
@ -68,7 +87,7 @@
android:id="@+id/list_properties" android:id="@+id/list_properties"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:listitem="@layout/card_simple_outlined" /> android:defaultFocusHighlightEnabled="false" />
</LinearLayout> </LinearLayout>

View File

@ -27,6 +27,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:defaultFocusHighlightEnabled="false"
tools:listitem="@layout/card_game" /> tools:listitem="@layout/card_game" />
</RelativeLayout> </RelativeLayout>

View File

@ -7,7 +7,8 @@
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:scrollbars="vertical" android:scrollbars="vertical"
android:fadeScrollbars="false" android:fadeScrollbars="false"
android:clipToPadding="false"> android:clipToPadding="false"
android:defaultFocusHighlightEnabled="false">
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/linear_layout_settings" android:id="@+id/linear_layout_settings"

View File

@ -10,12 +10,14 @@
android:id="@+id/appbar_installables" android:id="@+id/appbar_installables"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_installables" android:id="@+id/toolbar_installables"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:title="@string/manage_yuzu_data" app:title="@string/manage_yuzu_data"
app:navigationIcon="@drawable/ic_back" /> app:navigationIcon="@drawable/ic_back" />

View File

@ -10,12 +10,14 @@
android:id="@+id/appbar_licenses" android:id="@+id/appbar_licenses"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_licenses" android:id="@+id/toolbar_licenses"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:title="@string/licenses" app:title="@string/licenses"
app:navigationIcon="@drawable/ic_back" /> app:navigationIcon="@drawable/ic_back" />

View File

@ -11,6 +11,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
app:elevation="0dp"> app:elevation="0dp">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.CollapsingToolbarLayout
@ -24,6 +25,7 @@
android:id="@+id/toolbar_settings" android:id="@+id/toolbar_settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:layout_collapseMode="pin" app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_back" /> app:navigationIcon="@drawable/ic_back" />

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:focusable="true" android:focusable="false"
android:paddingHorizontal="20dp" android:paddingHorizontal="20dp"
android:paddingVertical="16dp"> android:paddingVertical="16dp">
@ -14,12 +14,11 @@
android:id="@+id/text_container" android:id="@+id/text_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:orientation="vertical" android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="@+id/addon_switch" android:layout_marginEnd="16dp"
app:layout_constraintEnd_toStartOf="@+id/addon_switch" app:layout_constraintEnd_toStartOf="@+id/addon_checkbox"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/addon_switch"> app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/title" android:id="@+id/title"
@ -42,16 +41,29 @@
</LinearLayout> </LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch <com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/addon_switch" android:id="@+id/addon_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center"
android:nextFocusLeft="@id/addon_container" android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="@+id/text_container"
app:layout_constraintBottom_toBottomOf="@+id/text_container"
app:layout_constraintEnd_toStartOf="@+id/button_delete" />
<Button
android:id="@+id/button_delete"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:contentDescription="@string/delete"
android:tooltipText="@string/delete"
app:icon="@drawable/ic_delete"
app:iconTint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_container" app:layout_constraintTop_toTopOf="@+id/addon_checkbox"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -69,7 +69,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:visibility="gone" android:visibility="gone"
android:text="@string/clear" android:text="@string/use_global_setting"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>

View File

@ -63,7 +63,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/clear" android:text="@string/use_global_setting"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@ -12,4 +12,5 @@
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:textStyle="bold" android:textStyle="bold"
android:focusable="false"
tools:text="CPU Settings" /> tools:text="CPU Settings" />

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_driver_use_global"
android:title="@string/use_global_setting" />
</menu>

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