From 91f533f86faa82fa61f1bb6411cd2d4bbedeb4a2 Mon Sep 17 00:00:00 2001 From: KorewaWatchful Date: Mon, 11 May 2026 13:47:44 -0400 Subject: [PATCH] emu: complete gui overhaul --- .github/workflows/c-cpp.yml | 100 +- .github/workflows/codeql-analysis.yml | 7 + .gitignore | 6 +- .gitmodules | 6 - CMakeLists.txt | 1 - android/.gitignore | 25 + android/{ => app}/assets/.gitignore | 0 android/app/build.gradle | 139 + android/app/proguard-rules.pro | 11 + .../{ => app}/src/main/AndroidManifest.xml | 73 +- .../java/org/vita3k/emulator/EmuSurface.java | 1 - .../java/org/vita3k/emulator/Emulator.java | 1278 ++++++ .../org/vita3k/emulator/InputDeviceUtils.kt | 61 + .../emulator/InstallForegroundService.kt | 371 ++ .../emulator/InstallServiceController.kt | 132 + .../java/org/vita3k/emulator/MainActivity.kt | 248 ++ .../java/org/vita3k/emulator/NativeLib.kt | 153 + .../java/org/vita3k/emulator/StorageAccess.kt | 138 + .../org/vita3k/emulator/Vita3KApplication.kt | 14 + .../vita3k/emulator/VitaInputConnection.java | 71 + .../org/vita3k/emulator/VitaTextEdit.java | 94 + .../java/org/vita3k/emulator/data/AppInfo.kt | 49 + .../org/vita3k/emulator/data/AppRepository.kt | 213 + .../org/vita3k/emulator/data/AppStorage.kt | 30 + .../vita3k/emulator/data/EmulatorConfig.kt | 339 ++ .../emulator/data/FirmwareInstallState.kt | 91 + .../org/vita3k/emulator/data/FirmwareLinks.kt | 45 + .../vita3k/emulator/data/InstallCallback.kt | 5 + .../vita3k/emulator/data/InstallRepository.kt | 45 + .../org/vita3k/emulator/data/NativeAppInfo.kt | 13 + .../vita3k/emulator/data/NativeImeState.kt | 12 + .../org/vita3k/emulator/data/NativeUser.kt | 7 + .../emulator/data/RestartRequiredSetting.kt | 31 + .../emulator/data/SettingsRepository.kt | 178 + .../org/vita3k/emulator/data/UiLanguages.kt | 45 + .../vita3k/emulator/data/UpdateCheckResult.kt | 31 + .../vita3k/emulator/data/UserRepository.kt | 24 + .../vita3k/emulator/overlay/InputOverlay.java | 434 +- .../overlay/InputOverlayDrawableButton.java | 0 .../overlay/InputOverlayDrawableDpad.java | 0 .../overlay/InputOverlayDrawableJoystick.java | 15 + .../vita3k/emulator/overlay/OverlayConfig.kt | 117 + .../emulator/overlay/OverlayLayoutStore.kt | 124 + .../java/org/vita3k/emulator/ui/AndroidUi.kt | 42 + .../vita3k/emulator/ui/components/HtmlText.kt | 48 + .../ui/components/OverlayEditorPalette.kt | 105 + .../emulator/ui/navigation/AppNavigation.kt | 349 ++ .../emulator/ui/screens/AppInfoSheet.kt | 150 + .../emulator/ui/screens/AppsListScreen.kt | 1379 ++++++ .../vita3k/emulator/ui/screens/FilterSheet.kt | 116 + .../emulator/ui/screens/InitialSetupScreen.kt | 528 +++ .../emulator/ui/screens/InstallSheet.kt | 273 ++ .../ui/screens/UserManagementScreen.kt | 418 ++ .../screens/emulation/EmulationPauseMenu.kt | 1015 +++++ .../ui/screens/emulation/NativeImeOverlay.kt | 233 ++ .../settings/ControllerMappingSettings.kt | 594 +++ .../settings/OverlayLayoutEditorDialog.kt | 149 + .../ui/screens/settings/OverlaySettings.kt | 217 + .../ui/screens/settings/SettingsComponents.kt | 647 +++ .../ui/screens/settings/SettingsModels.kt | 68 + .../ui/screens/settings/SettingsRoute.kt | 777 ++++ .../ui/screens/settings/SettingsSearch.kt | 188 + .../ui/screens/settings/SettingsSections.kt | 1771 ++++++++ .../org/vita3k/emulator/ui/theme/Theme.kt | 97 + .../vita3k/emulator/ui/viewmodel/AppAction.kt | 39 + .../ui/viewmodel/AppsListViewModel.kt | 344 ++ .../ui/viewmodel/EmulationSessionViewModel.kt | 315 ++ .../emulator/ui/viewmodel/InstallViewModel.kt | 247 ++ .../ui/viewmodel/SettingsViewModel.kt | 617 +++ .../ui/viewmodel/UserManagementViewModel.kt | 163 + .../src/main/res/drawable/button_circle.png | Bin .../res/drawable/button_circle_pressed.png | Bin .../src/main/res/drawable/button_cross.png | Bin .../res/drawable/button_cross_pressed.png | Bin .../src/main/res/drawable/button_hide.png | Bin .../main/res/drawable/button_hide_pressed.png | Bin .../src/main/res/drawable/button_l.png | Bin .../src/main/res/drawable/button_l2.png | Bin .../main/res/drawable/button_l2_pressed.png | Bin .../src/main/res/drawable/button_l3.png | Bin .../main/res/drawable/button_l3_pressed.png | Bin .../main/res/drawable/button_l_pressed.png | Bin .../src/main/res/drawable/button_ps.png | Bin .../main/res/drawable/button_ps_pressed.png | Bin .../src/main/res/drawable/button_r.png | Bin .../src/main/res/drawable/button_r2.png | Bin .../main/res/drawable/button_r2_pressed.png | Bin .../src/main/res/drawable/button_r3.png | Bin .../main/res/drawable/button_r3_pressed.png | Bin .../main/res/drawable/button_r_pressed.png | Bin .../src/main/res/drawable/button_select.png | Bin .../res/drawable/button_select_pressed.png | Bin .../src/main/res/drawable/button_square.png | Bin .../res/drawable/button_square_pressed.png | Bin .../src/main/res/drawable/button_start.png | Bin .../res/drawable/button_start_pressed.png | Bin .../src/main/res/drawable/button_touch_b.png | Bin .../src/main/res/drawable/button_touch_f.png | Bin .../src/main/res/drawable/button_triangle.png | Bin .../res/drawable/button_triangle_pressed.png | Bin .../src/main/res/drawable/dpad_idle.png | Bin .../src/main/res/drawable/dpad_up.png | Bin .../src/main/res/drawable/dpad_up_left.png | Bin .../src/main/res/drawable/joystick.png | Bin .../main/res/drawable/joystick_pressed.png | Bin .../src/main/res/drawable/joystick_range.png | Bin .../main/res/drawable/splash_background.xml | 9 + .../app/src/main/res/drawable/splash_icon.xml | 19 + .../src/main/res/mipmap/ic_launcher.png | Bin .../app/src/main/res/values-v31/styles.xml | 7 + android/app/src/main/res/values/colors.xml | 4 + .../src/main/res/values/integers.xml | 14 +- android/app/src/main/res/values/strings.xml | 715 ++++ android/app/src/main/res/values/styles.xml | 16 + .../app/src/main/res/xml/locales_config.xml | 4 + android/build.gradle | 116 +- .../gradle.properties | 0 .../gradle}/wrapper/gradle-wrapper.jar | Bin .../gradle}/wrapper/gradle-wrapper.properties | 0 gradlew => android/gradlew | 0 gradlew.bat => android/gradlew.bat | 0 android/proguard-rules.pro | 2 - settings.gradle => android/settings.gradle | 2 +- .../java/org/vita3k/emulator/Emulator.java | 348 -- .../provider/VitaDocumentsProvider.java | 204 - android/src/main/res/values/colors.xml | 6 - android/src/main/res/values/strings.xml | 3 - android/src/main/res/values/styles.xml | 4 - android/src/main/res/xml/file_paths.xml | 6 - appimage/build_updater.sh | 21 - build.gradle | 8 - cmake/qt6.cmake | 71 + external/CMakeLists.txt | 6 - external/ffmpeg | 2 +- external/imgui | 1 - external/imgui_club | 1 - i18n/lang/overlay_da.json | 101 + i18n/lang/overlay_de.json | 101 + i18n/lang/overlay_en-GB.json | 110 + i18n/lang/overlay_en.json | 158 + i18n/lang/overlay_es.json | 98 + i18n/lang/overlay_fi.json | 101 + i18n/lang/overlay_fr.json | 101 + i18n/lang/overlay_it.json | 98 + i18n/lang/overlay_ja.json | 104 + i18n/lang/overlay_ko.json | 101 + i18n/lang/overlay_nl.json | 101 + i18n/lang/overlay_no.json | 101 + i18n/lang/overlay_pl.json | 110 + i18n/lang/overlay_pt-BR.json | 104 + i18n/lang/overlay_pt-PT.json | 101 + i18n/lang/overlay_ru.json | 107 + i18n/lang/overlay_sv.json | 101 + i18n/lang/overlay_tr.json | 101 + i18n/lang/overlay_zh-Hans.json | 107 + i18n/lang/overlay_zh-Hant.json | 107 + i18n/qt/vita3k_en.ts | 3704 +++++++++++++++++ lang/system/da.xml | 397 -- lang/system/de.xml | 413 -- lang/system/en-gb.xml | 998 ----- lang/system/en.xml | 1006 ----- lang/system/es.xml | 436 -- lang/system/fi.xml | 402 -- lang/system/fr.xml | 517 --- lang/system/it.xml | 422 -- lang/system/ja.xml | 809 ---- lang/system/ko.xml | 436 -- lang/system/nl.xml | 418 -- lang/system/no.xml | 397 -- lang/system/pl.xml | 948 ----- lang/system/pt-br.xml | 489 --- lang/system/pt.xml | 401 -- lang/system/ru.xml | 890 ---- lang/system/sv.xml | 401 -- lang/system/tr.xml | 409 -- lang/system/zh-s.xml | 990 ----- lang/system/zh-t.xml | 990 ----- lang/user/id.xml | 855 ---- lang/user/ms.xml | 871 ---- lang/user/ua.xml | 865 ---- tools/i18n/generate_lang_catalog.py | 239 ++ vita3k/CMakeLists.txt | 174 +- vita3k/android/jni/android_state.cpp | 247 ++ vita3k/android/jni/android_state.h | 84 + .../jni}/filesystem_android.cpp | 101 +- vita3k/android/jni/ime.cpp | 142 + vita3k/android/jni/input_overlay.cpp | 120 + vita3k/android/jni/main_android.cpp | 521 +++ vita3k/android/jni/native_apps.cpp | 206 + vita3k/android/jni/native_bootstrap.cpp | 250 ++ vita3k/android/jni/native_config.cpp | 878 ++++ vita3k/android/jni/native_drivers.cpp | 252 ++ vita3k/android/jni/native_install.cpp | 150 + vita3k/android/jni/native_session.cpp | 186 + vita3k/android/jni/native_users.cpp | 122 + vita3k/app/CMakeLists.txt | 10 +- vita3k/app/include/app/discord.h | 8 +- vita3k/app/include/app/functions.h | 88 +- vita3k/app/include/app/session_controller.h | 93 + vita3k/app/include/app/state.h | 77 + vita3k/app/src/app.cpp | 460 +- vita3k/app/src/app_init.cpp | 675 ++- vita3k/app/src/apps_list.cpp | 598 +++ vita3k/app/src/discord.cpp | 44 +- vita3k/app/src/session_controller.cpp | 297 ++ vita3k/app/src/user.cpp | 187 + vita3k/archive.h | 47 + vita3k/audio/include/audio/impl/cubeb_audio.h | 1 + vita3k/audio/include/audio/impl/sdl_audio.h | 1 + vita3k/audio/include/audio/state.h | 9 + vita3k/audio/src/audio.cpp | 43 + vita3k/audio/src/impl/cubeb_audio.cpp | 16 + vita3k/audio/src/impl/sdl_audio.cpp | 15 + vita3k/bgm_player/CMakeLists.txt | 9 - vita3k/bgm_player/src/bgm_player.cpp | 462 -- vita3k/camera/include/camera/state.h | 2 + vita3k/camera/src/camera.cpp | 18 + vita3k/compat/CMakeLists.txt | 4 +- vita3k/compat/include/compat/functions.h | 15 +- vita3k/compat/include/compat/state.h | 20 +- vita3k/compat/src/compat.cpp | 273 +- vita3k/config/CMakeLists.txt | 6 +- vita3k/config/include/config/config.h | 161 +- vita3k/config/include/config/functions.h | 6 + vita3k/config/include/config/settings.h | 57 + vita3k/config/include/config/state.h | 13 +- vita3k/config/include/config/version.h | 1 + vita3k/config/src/config.cpp | 98 +- vita3k/config/src/settings.cpp | 413 ++ vita3k/config/src/version.cpp.in | 1 + vita3k/ctrl/CMakeLists.txt | 2 +- vita3k/ctrl/include/ctrl/functions.h | 2 +- vita3k/ctrl/include/ctrl/state.h | 36 + vita3k/ctrl/src/ctrl.cpp | 164 +- vita3k/dialog/CMakeLists.txt | 2 +- vita3k/dialog/include/dialog/state.h | 26 +- vita3k/display/include/display/state.h | 9 + vita3k/display/src/display.cpp | 41 +- vita3k/emuenv/CMakeLists.txt | 2 +- vita3k/emuenv/include/emuenv/state.h | 82 +- vita3k/emuenv/src/emuenv.cpp | 10 +- vita3k/glutil/include/glutil/object_array.h | 8 + vita3k/gui-qt/CMakeLists.txt | 120 + .../include/gui-qt/about_dialog.h} | 28 +- vita3k/gui-qt/include/gui-qt/app_icon_item.h | 72 + vita3k/gui-qt/include/gui-qt/apps_list.h | 65 + .../gui-qt/include/gui-qt/apps_list_columns.h | 95 + .../include/gui-qt/apps_list_context_menu.h | 78 + .../include/gui-qt/apps_list_delegate.h | 30 + .../gui-qt/include/gui-qt/apps_list_table.h | 99 + .../include/gui-qt/archive_install_dialog.h | 85 + .../gui-qt/include/gui-qt/controls_dialog.h | 254 ++ .../include/gui-qt/ctrl_keyboard_filter.h | 48 + .../include/gui-qt/custom_dock_widget.h | 62 + vita3k/gui-qt/include/gui-qt/debug_widget.h | 92 + .../include/gui-qt/firmware_install_dialog.h | 63 + .../include/gui-qt/game_compatibility.h | 54 + vita3k/gui-qt/include/gui-qt/game_window.h | 105 + vita3k/gui-qt/include/gui-qt/gui_language.h | 38 + vita3k/gui-qt/include/gui-qt/gui_save.h | 38 + vita3k/gui-qt/include/gui-qt/gui_settings.h | 97 + .../gui-qt/include/gui-qt/gui_settings_base.h | 60 + .../include/gui-qt/ime_keyboard_filter.h | 35 + .../include/gui-qt/license_install_dialog.h | 34 + .../gui-qt/include/gui-qt/live_area_widget.h | 137 + vita3k/gui-qt/include/gui-qt/log_widget.h | 73 + vita3k/gui-qt/include/gui-qt/main_window.h | 213 + .../include/gui-qt/persistent_settings.h | 47 + .../include/gui-qt/physical_key_qt.h} | 8 +- .../include/gui-qt/pkg_install_dialog.h | 73 + vita3k/gui-qt/include/gui-qt/qt_utils.h | 54 + .../gui-qt/include/gui-qt/settings_dialog.h | 121 + .../include/gui-qt/settings_dialog_tooltips.h | 114 + vita3k/gui-qt/include/gui-qt/stylesheets.h | 27 + .../include/gui-qt/trophy_collection_dialog.h | 184 + vita3k/gui-qt/include/gui-qt/update_manager.h | 40 + .../include/gui-qt/user_management_dialog.h | 48 + vita3k/gui-qt/include/gui-qt/welcome_dialog.h | 40 + vita3k/gui-qt/src/about_dialog.cpp | 78 + vita3k/gui-qt/src/about_dialog.ui | 402 ++ vita3k/gui-qt/src/apps_list.cpp | 140 + vita3k/gui-qt/src/apps_list_context_menu.cpp | 709 ++++ vita3k/gui-qt/src/apps_list_delegate.cpp | 110 + vita3k/gui-qt/src/apps_list_table.cpp | 733 ++++ vita3k/gui-qt/src/archive_install_dialog.cpp | 458 ++ vita3k/gui-qt/src/controls_dialog.cpp | 1315 ++++++ vita3k/gui-qt/src/controls_dialog.ui | 74 + vita3k/gui-qt/src/ctrl_keyboard_filter.cpp | 201 + vita3k/gui-qt/src/debug_widget.cpp | 503 +++ vita3k/gui-qt/src/firmware_install_dialog.cpp | 135 + vita3k/gui-qt/src/game_compatibility.cpp | 107 + vita3k/gui-qt/src/game_window.cpp | 450 ++ vita3k/gui-qt/src/gui_language.cpp | 148 + vita3k/gui-qt/src/gui_settings.cpp | 49 + vita3k/gui-qt/src/gui_settings_base.cpp | 103 + vita3k/gui-qt/src/ime_keyboard_filter.cpp | 168 + vita3k/gui-qt/src/license_install_dialog.cpp | 111 + vita3k/gui-qt/src/live_area_widget.cpp | 863 ++++ vita3k/gui-qt/src/log_widget.cpp | 530 +++ vita3k/gui-qt/src/main_window.cpp | 2163 ++++++++++ vita3k/gui-qt/src/main_window.ui | 401 ++ vita3k/gui-qt/src/persistent_settings.cpp | 85 + .../src/physical_key_qt.cpp} | 40 +- vita3k/gui-qt/src/pkg_install_dialog.cpp | 338 ++ vita3k/gui-qt/src/qt_utils.cpp | 134 + vita3k/gui-qt/src/settings_dialog.cpp | 1388 ++++++ vita3k/gui-qt/src/settings_dialog.ui | 3201 ++++++++++++++ .../gui-qt/src/settings_dialog_tooltips.cpp | 177 + vita3k/gui-qt/src/stylesheets.cpp | 321 ++ .../gui-qt/src/trophy_collection_dialog.cpp | 913 ++++ vita3k/gui-qt/src/update_manager.cpp | 389 ++ vita3k/gui-qt/src/user_management_dialog.cpp | 155 + vita3k/gui-qt/src/welcome_dialog.cpp | 180 + vita3k/gui-qt/src/welcome_dialog.ui | 279 ++ vita3k/gui/CMakeLists.txt | 76 - vita3k/gui/include/gui/functions.h | 151 - vita3k/gui/include/gui/imgui_impl_sdl.h | 38 - vita3k/gui/include/gui/imgui_impl_sdl_gl3.h | 46 - vita3k/gui/include/gui/imgui_impl_sdl_state.h | 73 - .../gui/include/gui/imgui_impl_sdl_vulkan.h | 101 - vita3k/gui/include/gui/state.h | 376 -- vita3k/gui/src/about_dialog.cpp | 177 - vita3k/gui/src/allocations_dialog.cpp | 70 - vita3k/gui/src/app_context_menu.cpp | 1096 ----- vita3k/gui/src/archive_install_dialog.cpp | 313 -- vita3k/gui/src/common_dialog.cpp | 656 --- vita3k/gui/src/compile_shaders.cpp | 102 - vita3k/gui/src/condvars_dialog.cpp | 56 - vita3k/gui/src/content_manager.cpp | 615 --- vita3k/gui/src/controllers_dialog.cpp | 462 -- vita3k/gui/src/controls_dialog.cpp | 262 -- vita3k/gui/src/disassembly_dialog.cpp | 134 - vita3k/gui/src/firmware_install_dialog.cpp | 146 - vita3k/gui/src/gui.cpp | 1119 ----- vita3k/gui/src/home_screen.cpp | 1014 ----- vita3k/gui/src/ime.cpp | 500 --- vita3k/gui/src/imgui_impl_sdl.cpp | 622 --- vita3k/gui/src/imgui_impl_sdl_gl3.cpp | 379 -- vita3k/gui/src/imgui_impl_sdl_vulkan.cpp | 834 ---- vita3k/gui/src/information_bar.cpp | 697 ---- vita3k/gui/src/initial_setup.cpp | 326 -- vita3k/gui/src/license_install_dialog.cpp | 159 - vita3k/gui/src/live_area.cpp | 1396 ------- vita3k/gui/src/macos_helper.mm | 141 - vita3k/gui/src/main_menubar.cpp | 184 - vita3k/gui/src/manual.cpp | 298 -- vita3k/gui/src/mutexes_dialog.cpp | 63 - vita3k/gui/src/overlay_dialog.cpp | 192 - vita3k/gui/src/perf_overlay.cpp | 93 - vita3k/gui/src/pkg_install_dialog.cpp | 247 -- vita3k/gui/src/private.h | 87 - vita3k/gui/src/reinstall.cpp | 72 - vita3k/gui/src/settings.cpp | 1097 ----- vita3k/gui/src/settings_dialog.cpp | 1692 -------- vita3k/gui/src/themes.cpp | 559 --- vita3k/gui/src/threads_dialog.cpp | 95 - vita3k/gui/src/trophy_collection.cpp | 899 ---- vita3k/gui/src/trophy_unlocked.cpp | 149 - vita3k/gui/src/user_management.cpp | 924 ---- vita3k/gui/src/vita3k_update.cpp | 529 --- vita3k/gui/src/welcome_dialog.cpp | 111 - vita3k/gxm/include/gxm/functions.h | 5 + vita3k/gxm/include/gxm/state.h | 26 + vita3k/host/CMakeLists.txt | 1 - vita3k/host/dialog/CMakeLists.txt | 22 - .../dialog/include/host/dialog/filesystem.h | 111 - vita3k/host/dialog/src/filesystem_nfd.cpp | 206 - vita3k/http/CMakeLists.txt | 11 +- vita3k/http/include/http/state.h | 3 + vita3k/http/src/http.cpp | 74 + vita3k/icons/PSV_Layout.svg | 429 ++ vita3k/icons/bronze.png | Bin 0 -> 49238 bytes vita3k/icons/chevron_down_dark.svg | 3 + vita3k/icons/chevron_down_light.svg | 3 + vita3k/icons/chevron_up_dark.svg | 3 + vita3k/icons/chevron_up_light.svg | 3 + vita3k/icons/configure.png | Bin 0 -> 13977 bytes vita3k/icons/controllers.png | Bin 0 -> 12939 bytes vita3k/icons/cross.png | Bin 0 -> 10201 bytes vita3k/icons/cross_dark.svg | 3 + vita3k/icons/cross_light.svg | 3 + vita3k/icons/doublearrow.png | Bin 0 -> 9044 bytes vita3k/icons/exit_fullscreen.png | Bin 0 -> 5876 bytes vita3k/icons/fullscreen.png | Bin 0 -> 6204 bytes vita3k/icons/gold.png | Bin 0 -> 58879 bytes vita3k/icons/info.png | Bin 0 -> 4113 bytes vita3k/icons/open.png | Bin 0 -> 5113 bytes vita3k/icons/pause.png | Bin 0 -> 4585 bytes vita3k/icons/platinum.png | Bin 0 -> 51632 bytes vita3k/icons/play.png | Bin 0 -> 8485 bytes vita3k/icons/refresh.png | Bin 0 -> 16592 bytes vita3k/icons/silver.png | Bin 0 -> 52928 bytes vita3k/icons/stop.png | Bin 0 -> 4075 bytes vita3k/ime/CMakeLists.txt | 9 +- vita3k/ime/include/ime/functions.h | 11 +- vita3k/ime/include/ime/keyboard.h | 37 + vita3k/ime/include/ime/state.h | 36 +- vita3k/ime/include/ime/types.h | 12 + vita3k/ime/src/ime.cpp | 137 + vita3k/input/CMakeLists.txt | 9 + vita3k/input/include/input/physical_key.h | 62 + .../input/include/input/physical_key_data.inc | 181 + vita3k/input/src/physical_key.cpp | 192 + vita3k/interface.cpp | 483 +-- vita3k/interface.h | 38 +- vita3k/io/include/io/functions.h | 1 + vita3k/io/src/io.cpp | 23 + vita3k/io/src/state_functions.cpp | 2 + vita3k/kernel/include/kernel/callback.h | 5 +- vita3k/kernel/include/kernel/debugger.h | 1 + vita3k/kernel/include/kernel/object_store.h | 6 + vita3k/kernel/include/kernel/state.h | 15 +- .../include/kernel/thread/thread_state.h | 2 + vita3k/kernel/src/debugger.cpp | 6 + vita3k/kernel/src/kernel.cpp | 147 +- vita3k/kernel/src/load_self.cpp | 3 + vita3k/kernel/src/sync_primitives.cpp | 69 +- vita3k/kernel/src/thread.cpp | 6 +- vita3k/lang/CMakeLists.txt | 43 +- vita3k/lang/include/lang/state.h | 928 +---- vita3k/lang/include/lang/strings.def | 40 + vita3k/lang/src/lang.cpp | 499 +-- vita3k/main.cpp | 500 +-- vita3k/mem/include/mem/functions.h | 1 + vita3k/mem/src/mem.cpp | 18 + vita3k/modules/CMakeLists.txt | 2 +- vita3k/modules/SceAppMgr/SceAppMgr.cpp | 21 +- .../SceCommonDialog/SceCommonDialog.cpp | 212 +- vita3k/modules/SceCtrl/SceCtrl.cpp | 2 +- vita3k/modules/SceGxm/SceGxm.cpp | 231 +- vita3k/modules/SceIme/SceIme.cpp | 31 +- vita3k/modules/SceIpmi/SceIpmi.cpp | 2 +- .../SceKernelThreadMgr/SceThreadmgr.cpp | 28 +- vita3k/modules/SceLibDbg/SceDbg.cpp | 2 +- vita3k/modules/SceLibKernel/SceLibKernel.cpp | 2 +- vita3k/modules/SceLibc/SceLibc.cpp | 4 +- vita3k/modules/SceNpTrophy/SceNpTrophy.cpp | 23 +- vita3k/modules/SceShellSvc/SceShellSvc.cpp | 5 +- vita3k/modules/SceTouch/SceTouch.cpp | 4 +- .../modules/include/modules/module_parent.h | 2 +- vita3k/modules/module_parent.cpp | 44 +- vita3k/motion/include/motion/functions.h | 3 + vita3k/motion/include/motion/state.h | 27 + vita3k/motion/src/motion.cpp | 115 +- vita3k/net/CMakeLists.txt | 1 + vita3k/net/include/net/state.h | 18 +- .../semaphores_dialog.cpp => net/src/net.cpp} | 48 +- vita3k/ngs/include/ngs/modules/atrac9.h | 2 + vita3k/ngs/include/ngs/modules/player.h | 1 + vita3k/ngs/include/ngs/state.h | 1 + vita3k/ngs/include/ngs/system.h | 1 + vita3k/ngs/src/modules/atrac9.cpp | 19 + vita3k/ngs/src/modules/player.cpp | 8 + vita3k/ngs/src/ngs.cpp | 33 +- vita3k/np/include/np/state.h | 13 +- vita3k/np/src/init.cpp | 15 +- vita3k/overlay/CMakeLists.txt | 40 + vita3k/overlay/include/overlay/animation.h | 116 + .../overlay/include/overlay/common_dialog.h | 250 ++ .../include/overlay/compiled_resource.h | 161 + vita3k/overlay/include/overlay/controls.h | 186 + .../overlay/include/overlay/display_manager.h | 224 + vita3k/overlay/include/overlay/element.h | 123 + vita3k/overlay/include/overlay/font.h | 191 + vita3k/overlay/include/overlay/input.h | 70 + vita3k/overlay/include/overlay/layouts.h | 89 + vita3k/overlay/include/overlay/overlay.h | 107 + .../overlay/include/overlay/pause_overlay.h | 43 + vita3k/overlay/include/overlay/perf_overlay.h | 89 + .../include/overlay/shader_compile_notice.h | 50 + .../overlay/shader_precompile_progress.h | 69 + .../include/overlay/trophy_notification.h | 78 + vita3k/overlay/include/overlay/types.h | 247 ++ .../overlay/include/overlay/user_interface.h | 103 + vita3k/overlay/src/animation.cpp | 242 ++ vita3k/overlay/src/common_dialog.cpp | 1783 ++++++++ vita3k/overlay/src/compiled_resource.cpp | 174 + vita3k/overlay/src/controls.cpp | 494 +++ vita3k/overlay/src/display_manager.cpp | 317 ++ vita3k/overlay/src/element.cpp | 408 ++ vita3k/overlay/src/font.cpp | 530 +++ vita3k/overlay/src/input.cpp | 48 + vita3k/overlay/src/layouts.cpp | 216 + vita3k/overlay/src/overlay.cpp | 185 + vita3k/overlay/src/pause_overlay.cpp | 79 + vita3k/overlay/src/perf_overlay.cpp | 195 + vita3k/overlay/src/shader_compile_notice.cpp | 82 + .../src/shader_precompile_progress.cpp | 178 + vita3k/overlay/src/trophy_notification.cpp | 181 + vita3k/packages/include/packages/license.h | 1 + vita3k/packages/include/packages/pkg.h | 2 +- vita3k/packages/src/license.cpp | 26 + vita3k/packages/src/pkg.cpp | 27 + vita3k/regmgr/include/regmgr/state.h | 15 + vita3k/regmgr/src/regmgr.cpp | 60 +- vita3k/renderer/CMakeLists.txt | 21 +- vita3k/renderer/include/renderer/commands.h | 2 + .../include/renderer/driver_functions.h | 1 + vita3k/renderer/include/renderer/functions.h | 27 +- .../renderer/include/renderer/gl/functions.h | 2 +- .../include/renderer/gl/overlay_renderer.h | 96 + .../include/renderer/gl/screen_render.h | 2 +- vita3k/renderer/include/renderer/gl/state.h | 17 +- .../include/renderer/gl/surface_cache.h | 2 + vita3k/renderer/include/renderer/gl/types.h | 5 +- vita3k/renderer/include/renderer/state.h | 114 +- .../renderer/include/renderer/texture_cache.h | 10 + .../include/renderer/vulkan/functions.h | 2 +- .../renderer/vulkan/overlay_renderer.h | 129 + .../include/renderer/vulkan/pipeline_cache.h | 4 + .../include/renderer/vulkan/screen_renderer.h | 21 +- .../renderer/include/renderer/vulkan/state.h | 22 +- .../include/renderer/vulkan/surface_cache.h | 1 + .../renderer/include/renderer/vulkan/types.h | 5 +- vita3k/renderer/src/batch.cpp | 137 +- vita3k/renderer/src/creation.cpp | 12 +- vita3k/renderer/src/gl/color_formats.cpp | 4 + vita3k/renderer/src/gl/overlay_renderer.cpp | 525 +++ vita3k/renderer/src/gl/renderer.cpp | 259 +- vita3k/renderer/src/gl/screen_render.cpp | 7 +- vita3k/renderer/src/gl/surface_cache.cpp | 20 + vita3k/renderer/src/gl/texture.cpp | 18 + vita3k/renderer/src/renderer.cpp | 166 +- vita3k/renderer/src/scene.cpp | 22 +- vita3k/renderer/src/sync.cpp | 34 +- vita3k/renderer/src/texture/cache.cpp | 2 +- vita3k/renderer/src/texture/replacement.cpp | 1 + vita3k/renderer/src/texture/yuv.cpp | 39 +- vita3k/renderer/src/vulkan/context.cpp | 22 +- vita3k/renderer/src/vulkan/creation.cpp | 23 +- vita3k/renderer/src/vulkan/metal_layer.mm | 23 + .../renderer/src/vulkan/overlay_renderer.cpp | 1065 +++++ vita3k/renderer/src/vulkan/pipeline_cache.cpp | 71 +- vita3k/renderer/src/vulkan/renderer.cpp | 670 ++- .../renderer/src/vulkan/screen_renderer.cpp | 315 +- vita3k/renderer/src/vulkan/surface_cache.cpp | 59 + vita3k/renderer/src/vulkan/texture.cpp | 25 + vita3k/resources.qrc | 26 + vita3k/script/update-linux.sh | 49 - vita3k/script/update-macos.sh | 22 - vita3k/script/update-windows.bat | 82 - .../shaders-builtin/opengl/render_main.frag | 4 +- vita3k/shaders-builtin/overlay/overlay.frag | 219 + .../shaders-builtin/overlay/overlay.frag.spv | Bin 0 -> 17724 bytes vita3k/shaders-builtin/overlay/overlay.vert | 72 + .../shaders-builtin/overlay/overlay.vert.spv | Bin 0 -> 5224 bytes .../shaders-builtin/overlay/overlay_vk.frag | 233 ++ .../shaders-builtin/overlay/overlay_vk.vert | 83 + vita3k/threads/include/threads/queue.h | 8 + vita3k/touch/CMakeLists.txt | 2 +- vita3k/touch/include/touch/functions.h | 24 +- vita3k/touch/include/touch/state.h | 61 + vita3k/touch/src/touch.cpp | 283 +- vita3k/updater/CMakeLists.txt | 10 + .../include/updater}/functions.h | 20 +- vita3k/updater/include/updater/state.h | 74 + vita3k/updater/src/common.cpp | 31 + vita3k/util/CMakeLists.txt | 2 + vita3k/util/include/util/android_driver.h | 18 + vita3k/util/include/util/log.h | 2 + vita3k/util/include/util/net_utils.h | 18 - vita3k/util/src/android_driver.cpp | 494 +++ vita3k/util/src/logging.cpp | 67 +- vita3k/util/src/net_utils.cpp | 99 - vita3k/vkutil/include/vkutil/objects.h | 1 + vita3k/vkutil/include/vkutil/vkutil.h | 64 + vita3k/vkutil/src/objects.cpp | 4 + 567 files changed, 64356 insertions(+), 40471 deletions(-) create mode 100644 android/.gitignore rename android/{ => app}/assets/.gitignore (100%) create mode 100644 android/app/build.gradle create mode 100644 android/app/proguard-rules.pro rename android/{ => app}/src/main/AndroidManifest.xml (73%) rename android/{ => app}/src/main/java/org/vita3k/emulator/EmuSurface.java (99%) create mode 100644 android/app/src/main/java/org/vita3k/emulator/Emulator.java create mode 100644 android/app/src/main/java/org/vita3k/emulator/InputDeviceUtils.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/InstallForegroundService.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/InstallServiceController.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/MainActivity.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/NativeLib.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/StorageAccess.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/Vita3KApplication.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/VitaInputConnection.java create mode 100644 android/app/src/main/java/org/vita3k/emulator/VitaTextEdit.java create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/AppInfo.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/AppRepository.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/AppStorage.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/EmulatorConfig.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/FirmwareInstallState.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/FirmwareLinks.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/InstallCallback.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/InstallRepository.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/NativeAppInfo.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/NativeImeState.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/NativeUser.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/RestartRequiredSetting.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/SettingsRepository.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/UiLanguages.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/UpdateCheckResult.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/data/UserRepository.kt rename android/{ => app}/src/main/java/org/vita3k/emulator/overlay/InputOverlay.java (73%) rename android/{ => app}/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableButton.java (100%) rename android/{ => app}/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableDpad.java (100%) rename android/{ => app}/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableJoystick.java (95%) create mode 100644 android/app/src/main/java/org/vita3k/emulator/overlay/OverlayConfig.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/overlay/OverlayLayoutStore.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/AndroidUi.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/components/HtmlText.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/components/OverlayEditorPalette.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/navigation/AppNavigation.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/AppInfoSheet.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/AppsListScreen.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/FilterSheet.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/InitialSetupScreen.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/InstallSheet.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/UserManagementScreen.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/EmulationPauseMenu.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/NativeImeOverlay.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/ControllerMappingSettings.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlayLayoutEditorDialog.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlaySettings.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsComponents.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsModels.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsRoute.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSearch.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSections.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/theme/Theme.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppAction.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppsListViewModel.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/EmulationSessionViewModel.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/InstallViewModel.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/SettingsViewModel.kt create mode 100644 android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/UserManagementViewModel.kt rename android/{ => app}/src/main/res/drawable/button_circle.png (100%) rename android/{ => app}/src/main/res/drawable/button_circle_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_cross.png (100%) rename android/{ => app}/src/main/res/drawable/button_cross_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_hide.png (100%) rename android/{ => app}/src/main/res/drawable/button_hide_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_l.png (100%) rename android/{ => app}/src/main/res/drawable/button_l2.png (100%) rename android/{ => app}/src/main/res/drawable/button_l2_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_l3.png (100%) rename android/{ => app}/src/main/res/drawable/button_l3_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_l_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_ps.png (100%) rename android/{ => app}/src/main/res/drawable/button_ps_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_r.png (100%) rename android/{ => app}/src/main/res/drawable/button_r2.png (100%) rename android/{ => app}/src/main/res/drawable/button_r2_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_r3.png (100%) rename android/{ => app}/src/main/res/drawable/button_r3_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_r_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_select.png (100%) rename android/{ => app}/src/main/res/drawable/button_select_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_square.png (100%) rename android/{ => app}/src/main/res/drawable/button_square_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_start.png (100%) rename android/{ => app}/src/main/res/drawable/button_start_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/button_touch_b.png (100%) rename android/{ => app}/src/main/res/drawable/button_touch_f.png (100%) rename android/{ => app}/src/main/res/drawable/button_triangle.png (100%) rename android/{ => app}/src/main/res/drawable/button_triangle_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/dpad_idle.png (100%) rename android/{ => app}/src/main/res/drawable/dpad_up.png (100%) rename android/{ => app}/src/main/res/drawable/dpad_up_left.png (100%) rename android/{ => app}/src/main/res/drawable/joystick.png (100%) rename android/{ => app}/src/main/res/drawable/joystick_pressed.png (100%) rename android/{ => app}/src/main/res/drawable/joystick_range.png (100%) create mode 100644 android/app/src/main/res/drawable/splash_background.xml create mode 100644 android/app/src/main/res/drawable/splash_icon.xml rename android/{ => app}/src/main/res/mipmap/ic_launcher.png (100%) create mode 100644 android/app/src/main/res/values-v31/styles.xml create mode 100644 android/app/src/main/res/values/colors.xml rename android/{ => app}/src/main/res/values/integers.xml (80%) create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/main/res/xml/locales_config.xml rename gradle.properties => android/gradle.properties (100%) rename {gradle => android/gradle}/wrapper/gradle-wrapper.jar (100%) rename {gradle => android/gradle}/wrapper/gradle-wrapper.properties (100%) rename gradlew => android/gradlew (100%) mode change 100755 => 100644 rename gradlew.bat => android/gradlew.bat (100%) delete mode 100644 android/proguard-rules.pro rename settings.gradle => android/settings.gradle (93%) delete mode 100644 android/src/main/java/org/vita3k/emulator/Emulator.java delete mode 100644 android/src/main/java/org/vita3k/emulator/provider/VitaDocumentsProvider.java delete mode 100644 android/src/main/res/values/colors.xml delete mode 100644 android/src/main/res/values/strings.xml delete mode 100644 android/src/main/res/values/styles.xml delete mode 100644 android/src/main/res/xml/file_paths.xml delete mode 100755 appimage/build_updater.sh delete mode 100644 build.gradle create mode 100644 cmake/qt6.cmake delete mode 160000 external/imgui delete mode 160000 external/imgui_club create mode 100644 i18n/lang/overlay_da.json create mode 100644 i18n/lang/overlay_de.json create mode 100644 i18n/lang/overlay_en-GB.json create mode 100644 i18n/lang/overlay_en.json create mode 100644 i18n/lang/overlay_es.json create mode 100644 i18n/lang/overlay_fi.json create mode 100644 i18n/lang/overlay_fr.json create mode 100644 i18n/lang/overlay_it.json create mode 100644 i18n/lang/overlay_ja.json create mode 100644 i18n/lang/overlay_ko.json create mode 100644 i18n/lang/overlay_nl.json create mode 100644 i18n/lang/overlay_no.json create mode 100644 i18n/lang/overlay_pl.json create mode 100644 i18n/lang/overlay_pt-BR.json create mode 100644 i18n/lang/overlay_pt-PT.json create mode 100644 i18n/lang/overlay_ru.json create mode 100644 i18n/lang/overlay_sv.json create mode 100644 i18n/lang/overlay_tr.json create mode 100644 i18n/lang/overlay_zh-Hans.json create mode 100644 i18n/lang/overlay_zh-Hant.json create mode 100644 i18n/qt/vita3k_en.ts delete mode 100644 lang/system/da.xml delete mode 100644 lang/system/de.xml delete mode 100644 lang/system/en-gb.xml delete mode 100644 lang/system/en.xml delete mode 100644 lang/system/es.xml delete mode 100644 lang/system/fi.xml delete mode 100644 lang/system/fr.xml delete mode 100644 lang/system/it.xml delete mode 100644 lang/system/ja.xml delete mode 100644 lang/system/ko.xml delete mode 100644 lang/system/nl.xml delete mode 100644 lang/system/no.xml delete mode 100644 lang/system/pl.xml delete mode 100644 lang/system/pt-br.xml delete mode 100644 lang/system/pt.xml delete mode 100644 lang/system/ru.xml delete mode 100644 lang/system/sv.xml delete mode 100644 lang/system/tr.xml delete mode 100644 lang/system/zh-s.xml delete mode 100644 lang/system/zh-t.xml delete mode 100644 lang/user/id.xml delete mode 100644 lang/user/ms.xml delete mode 100644 lang/user/ua.xml create mode 100644 tools/i18n/generate_lang_catalog.py create mode 100644 vita3k/android/jni/android_state.cpp create mode 100644 vita3k/android/jni/android_state.h rename vita3k/{host/dialog/src => android/jni}/filesystem_android.cpp (51%) create mode 100644 vita3k/android/jni/ime.cpp create mode 100644 vita3k/android/jni/input_overlay.cpp create mode 100644 vita3k/android/jni/main_android.cpp create mode 100644 vita3k/android/jni/native_apps.cpp create mode 100644 vita3k/android/jni/native_bootstrap.cpp create mode 100644 vita3k/android/jni/native_config.cpp create mode 100644 vita3k/android/jni/native_drivers.cpp create mode 100644 vita3k/android/jni/native_install.cpp create mode 100644 vita3k/android/jni/native_session.cpp create mode 100644 vita3k/android/jni/native_users.cpp create mode 100644 vita3k/app/include/app/session_controller.h create mode 100644 vita3k/app/include/app/state.h create mode 100644 vita3k/app/src/apps_list.cpp create mode 100644 vita3k/app/src/session_controller.cpp create mode 100644 vita3k/app/src/user.cpp create mode 100644 vita3k/archive.h delete mode 100644 vita3k/bgm_player/CMakeLists.txt delete mode 100644 vita3k/bgm_player/src/bgm_player.cpp create mode 100644 vita3k/config/include/config/settings.h create mode 100644 vita3k/config/src/settings.cpp create mode 100644 vita3k/gui-qt/CMakeLists.txt rename vita3k/{gui/include/gui/macos_helper.h => gui-qt/include/gui-qt/about_dialog.h} (69%) create mode 100644 vita3k/gui-qt/include/gui-qt/app_icon_item.h create mode 100644 vita3k/gui-qt/include/gui-qt/apps_list.h create mode 100644 vita3k/gui-qt/include/gui-qt/apps_list_columns.h create mode 100644 vita3k/gui-qt/include/gui-qt/apps_list_context_menu.h create mode 100644 vita3k/gui-qt/include/gui-qt/apps_list_delegate.h create mode 100644 vita3k/gui-qt/include/gui-qt/apps_list_table.h create mode 100644 vita3k/gui-qt/include/gui-qt/archive_install_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/controls_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/ctrl_keyboard_filter.h create mode 100644 vita3k/gui-qt/include/gui-qt/custom_dock_widget.h create mode 100644 vita3k/gui-qt/include/gui-qt/debug_widget.h create mode 100644 vita3k/gui-qt/include/gui-qt/firmware_install_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/game_compatibility.h create mode 100644 vita3k/gui-qt/include/gui-qt/game_window.h create mode 100644 vita3k/gui-qt/include/gui-qt/gui_language.h create mode 100644 vita3k/gui-qt/include/gui-qt/gui_save.h create mode 100644 vita3k/gui-qt/include/gui-qt/gui_settings.h create mode 100644 vita3k/gui-qt/include/gui-qt/gui_settings_base.h create mode 100644 vita3k/gui-qt/include/gui-qt/ime_keyboard_filter.h create mode 100644 vita3k/gui-qt/include/gui-qt/license_install_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/live_area_widget.h create mode 100644 vita3k/gui-qt/include/gui-qt/log_widget.h create mode 100644 vita3k/gui-qt/include/gui-qt/main_window.h create mode 100644 vita3k/gui-qt/include/gui-qt/persistent_settings.h rename vita3k/{lang/include/lang/functions.h => gui-qt/include/gui-qt/physical_key_qt.h} (84%) create mode 100644 vita3k/gui-qt/include/gui-qt/pkg_install_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/qt_utils.h create mode 100644 vita3k/gui-qt/include/gui-qt/settings_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/settings_dialog_tooltips.h create mode 100644 vita3k/gui-qt/include/gui-qt/stylesheets.h create mode 100644 vita3k/gui-qt/include/gui-qt/trophy_collection_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/update_manager.h create mode 100644 vita3k/gui-qt/include/gui-qt/user_management_dialog.h create mode 100644 vita3k/gui-qt/include/gui-qt/welcome_dialog.h create mode 100644 vita3k/gui-qt/src/about_dialog.cpp create mode 100644 vita3k/gui-qt/src/about_dialog.ui create mode 100644 vita3k/gui-qt/src/apps_list.cpp create mode 100644 vita3k/gui-qt/src/apps_list_context_menu.cpp create mode 100644 vita3k/gui-qt/src/apps_list_delegate.cpp create mode 100644 vita3k/gui-qt/src/apps_list_table.cpp create mode 100644 vita3k/gui-qt/src/archive_install_dialog.cpp create mode 100644 vita3k/gui-qt/src/controls_dialog.cpp create mode 100644 vita3k/gui-qt/src/controls_dialog.ui create mode 100644 vita3k/gui-qt/src/ctrl_keyboard_filter.cpp create mode 100644 vita3k/gui-qt/src/debug_widget.cpp create mode 100644 vita3k/gui-qt/src/firmware_install_dialog.cpp create mode 100644 vita3k/gui-qt/src/game_compatibility.cpp create mode 100644 vita3k/gui-qt/src/game_window.cpp create mode 100644 vita3k/gui-qt/src/gui_language.cpp create mode 100644 vita3k/gui-qt/src/gui_settings.cpp create mode 100644 vita3k/gui-qt/src/gui_settings_base.cpp create mode 100644 vita3k/gui-qt/src/ime_keyboard_filter.cpp create mode 100644 vita3k/gui-qt/src/license_install_dialog.cpp create mode 100644 vita3k/gui-qt/src/live_area_widget.cpp create mode 100644 vita3k/gui-qt/src/log_widget.cpp create mode 100644 vita3k/gui-qt/src/main_window.cpp create mode 100644 vita3k/gui-qt/src/main_window.ui create mode 100644 vita3k/gui-qt/src/persistent_settings.cpp rename vita3k/{gui/src/eventflags_dialog.cpp => gui-qt/src/physical_key_qt.cpp} (50%) create mode 100644 vita3k/gui-qt/src/pkg_install_dialog.cpp create mode 100644 vita3k/gui-qt/src/qt_utils.cpp create mode 100644 vita3k/gui-qt/src/settings_dialog.cpp create mode 100644 vita3k/gui-qt/src/settings_dialog.ui create mode 100644 vita3k/gui-qt/src/settings_dialog_tooltips.cpp create mode 100644 vita3k/gui-qt/src/stylesheets.cpp create mode 100644 vita3k/gui-qt/src/trophy_collection_dialog.cpp create mode 100644 vita3k/gui-qt/src/update_manager.cpp create mode 100644 vita3k/gui-qt/src/user_management_dialog.cpp create mode 100644 vita3k/gui-qt/src/welcome_dialog.cpp create mode 100644 vita3k/gui-qt/src/welcome_dialog.ui delete mode 100644 vita3k/gui/CMakeLists.txt delete mode 100644 vita3k/gui/include/gui/functions.h delete mode 100644 vita3k/gui/include/gui/imgui_impl_sdl.h delete mode 100644 vita3k/gui/include/gui/imgui_impl_sdl_gl3.h delete mode 100644 vita3k/gui/include/gui/imgui_impl_sdl_state.h delete mode 100644 vita3k/gui/include/gui/imgui_impl_sdl_vulkan.h delete mode 100644 vita3k/gui/include/gui/state.h delete mode 100644 vita3k/gui/src/about_dialog.cpp delete mode 100644 vita3k/gui/src/allocations_dialog.cpp delete mode 100644 vita3k/gui/src/app_context_menu.cpp delete mode 100644 vita3k/gui/src/archive_install_dialog.cpp delete mode 100644 vita3k/gui/src/common_dialog.cpp delete mode 100644 vita3k/gui/src/compile_shaders.cpp delete mode 100644 vita3k/gui/src/condvars_dialog.cpp delete mode 100644 vita3k/gui/src/content_manager.cpp delete mode 100644 vita3k/gui/src/controllers_dialog.cpp delete mode 100644 vita3k/gui/src/controls_dialog.cpp delete mode 100644 vita3k/gui/src/disassembly_dialog.cpp delete mode 100644 vita3k/gui/src/firmware_install_dialog.cpp delete mode 100644 vita3k/gui/src/gui.cpp delete mode 100644 vita3k/gui/src/home_screen.cpp delete mode 100644 vita3k/gui/src/ime.cpp delete mode 100644 vita3k/gui/src/imgui_impl_sdl.cpp delete mode 100644 vita3k/gui/src/imgui_impl_sdl_gl3.cpp delete mode 100644 vita3k/gui/src/imgui_impl_sdl_vulkan.cpp delete mode 100644 vita3k/gui/src/information_bar.cpp delete mode 100644 vita3k/gui/src/initial_setup.cpp delete mode 100644 vita3k/gui/src/license_install_dialog.cpp delete mode 100644 vita3k/gui/src/live_area.cpp delete mode 100644 vita3k/gui/src/macos_helper.mm delete mode 100644 vita3k/gui/src/main_menubar.cpp delete mode 100644 vita3k/gui/src/manual.cpp delete mode 100644 vita3k/gui/src/mutexes_dialog.cpp delete mode 100644 vita3k/gui/src/overlay_dialog.cpp delete mode 100644 vita3k/gui/src/perf_overlay.cpp delete mode 100644 vita3k/gui/src/pkg_install_dialog.cpp delete mode 100644 vita3k/gui/src/private.h delete mode 100644 vita3k/gui/src/reinstall.cpp delete mode 100644 vita3k/gui/src/settings.cpp delete mode 100644 vita3k/gui/src/settings_dialog.cpp delete mode 100644 vita3k/gui/src/themes.cpp delete mode 100644 vita3k/gui/src/threads_dialog.cpp delete mode 100644 vita3k/gui/src/trophy_collection.cpp delete mode 100644 vita3k/gui/src/trophy_unlocked.cpp delete mode 100644 vita3k/gui/src/user_management.cpp delete mode 100644 vita3k/gui/src/vita3k_update.cpp delete mode 100644 vita3k/gui/src/welcome_dialog.cpp delete mode 100644 vita3k/host/CMakeLists.txt delete mode 100644 vita3k/host/dialog/CMakeLists.txt delete mode 100644 vita3k/host/dialog/include/host/dialog/filesystem.h delete mode 100644 vita3k/host/dialog/src/filesystem_nfd.cpp create mode 100644 vita3k/http/src/http.cpp create mode 100644 vita3k/icons/PSV_Layout.svg create mode 100644 vita3k/icons/bronze.png create mode 100644 vita3k/icons/chevron_down_dark.svg create mode 100644 vita3k/icons/chevron_down_light.svg create mode 100644 vita3k/icons/chevron_up_dark.svg create mode 100644 vita3k/icons/chevron_up_light.svg create mode 100644 vita3k/icons/configure.png create mode 100644 vita3k/icons/controllers.png create mode 100644 vita3k/icons/cross.png create mode 100644 vita3k/icons/cross_dark.svg create mode 100644 vita3k/icons/cross_light.svg create mode 100644 vita3k/icons/doublearrow.png create mode 100644 vita3k/icons/exit_fullscreen.png create mode 100644 vita3k/icons/fullscreen.png create mode 100644 vita3k/icons/gold.png create mode 100644 vita3k/icons/info.png create mode 100644 vita3k/icons/open.png create mode 100644 vita3k/icons/pause.png create mode 100644 vita3k/icons/platinum.png create mode 100644 vita3k/icons/play.png create mode 100644 vita3k/icons/refresh.png create mode 100644 vita3k/icons/silver.png create mode 100644 vita3k/icons/stop.png create mode 100644 vita3k/ime/include/ime/keyboard.h create mode 100644 vita3k/ime/src/ime.cpp create mode 100644 vita3k/input/CMakeLists.txt create mode 100644 vita3k/input/include/input/physical_key.h create mode 100644 vita3k/input/include/input/physical_key_data.inc create mode 100644 vita3k/input/src/physical_key.cpp create mode 100644 vita3k/lang/include/lang/strings.def rename vita3k/{gui/src/semaphores_dialog.cpp => net/src/net.cpp} (51%) create mode 100644 vita3k/overlay/CMakeLists.txt create mode 100644 vita3k/overlay/include/overlay/animation.h create mode 100644 vita3k/overlay/include/overlay/common_dialog.h create mode 100644 vita3k/overlay/include/overlay/compiled_resource.h create mode 100644 vita3k/overlay/include/overlay/controls.h create mode 100644 vita3k/overlay/include/overlay/display_manager.h create mode 100644 vita3k/overlay/include/overlay/element.h create mode 100644 vita3k/overlay/include/overlay/font.h create mode 100644 vita3k/overlay/include/overlay/input.h create mode 100644 vita3k/overlay/include/overlay/layouts.h create mode 100644 vita3k/overlay/include/overlay/overlay.h create mode 100644 vita3k/overlay/include/overlay/pause_overlay.h create mode 100644 vita3k/overlay/include/overlay/perf_overlay.h create mode 100644 vita3k/overlay/include/overlay/shader_compile_notice.h create mode 100644 vita3k/overlay/include/overlay/shader_precompile_progress.h create mode 100644 vita3k/overlay/include/overlay/trophy_notification.h create mode 100644 vita3k/overlay/include/overlay/types.h create mode 100644 vita3k/overlay/include/overlay/user_interface.h create mode 100644 vita3k/overlay/src/animation.cpp create mode 100644 vita3k/overlay/src/common_dialog.cpp create mode 100644 vita3k/overlay/src/compiled_resource.cpp create mode 100644 vita3k/overlay/src/controls.cpp create mode 100644 vita3k/overlay/src/display_manager.cpp create mode 100644 vita3k/overlay/src/element.cpp create mode 100644 vita3k/overlay/src/font.cpp create mode 100644 vita3k/overlay/src/input.cpp create mode 100644 vita3k/overlay/src/layouts.cpp create mode 100644 vita3k/overlay/src/overlay.cpp create mode 100644 vita3k/overlay/src/pause_overlay.cpp create mode 100644 vita3k/overlay/src/perf_overlay.cpp create mode 100644 vita3k/overlay/src/shader_compile_notice.cpp create mode 100644 vita3k/overlay/src/shader_precompile_progress.cpp create mode 100644 vita3k/overlay/src/trophy_notification.cpp create mode 100644 vita3k/renderer/include/renderer/gl/overlay_renderer.h create mode 100644 vita3k/renderer/include/renderer/vulkan/overlay_renderer.h create mode 100644 vita3k/renderer/src/gl/overlay_renderer.cpp create mode 100644 vita3k/renderer/src/vulkan/metal_layer.mm create mode 100644 vita3k/renderer/src/vulkan/overlay_renderer.cpp create mode 100644 vita3k/resources.qrc delete mode 100755 vita3k/script/update-linux.sh delete mode 100755 vita3k/script/update-macos.sh delete mode 100644 vita3k/script/update-windows.bat create mode 100644 vita3k/shaders-builtin/overlay/overlay.frag create mode 100644 vita3k/shaders-builtin/overlay/overlay.frag.spv create mode 100644 vita3k/shaders-builtin/overlay/overlay.vert create mode 100644 vita3k/shaders-builtin/overlay/overlay.vert.spv create mode 100644 vita3k/shaders-builtin/overlay/overlay_vk.frag create mode 100644 vita3k/shaders-builtin/overlay/overlay_vk.vert create mode 100644 vita3k/updater/CMakeLists.txt rename vita3k/{bgm_player/include/bgm_player => updater/include/updater}/functions.h (68%) create mode 100644 vita3k/updater/include/updater/state.h create mode 100644 vita3k/updater/src/common.cpp create mode 100644 vita3k/util/include/util/android_driver.h create mode 100644 vita3k/util/src/android_driver.cpp diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 70c7882f4..b92ac20db 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -51,13 +51,9 @@ jobs: cmake_preset: windows-ninja - os: windows-11-arm - arch: arm64 - cache_path: | - C:\vcpkg\installed - C:\vcpkg\packages - vcpkg_triplet: arm64-windows-static-md - extra_cmake_args: -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=arm64-windows-static-md - cmake_preset: windows-ninja-clang + cache_path: C:\msys2-ccache + msys2_env: CLANGARM64 + cmake_preset: windows-ninja - os: macos-intel runs_on: macos-latest @@ -112,17 +108,22 @@ jobs: curl -sLO https://github.com/linuxdeploy/linuxdeploy/releases/latest/download/linuxdeploy-${{ matrix.arch }}.AppImage sudo cp -f linuxdeploy-${{ matrix.arch }}.AppImage /usr/local/bin/ sudo chmod +x /usr/local/bin/linuxdeploy-${{ matrix.arch }}.AppImage + + # Set up linuxdeploy-plugin-qt for Qt bundling in AppImage + curl -sLO https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/latest/download/linuxdeploy-plugin-qt-${{ matrix.arch }}.AppImage + sudo cp -f linuxdeploy-plugin-qt-${{ matrix.arch }}.AppImage /usr/local/bin/ + sudo chmod +x /usr/local/bin/linuxdeploy-plugin-qt-${{ matrix.arch }}.AppImage if: startsWith(matrix.os, 'ubuntu') - name: Set up build environment (Windows) run: | vcpkg install zlib boost-system boost-filesystem boost-program-options boost-icl boost-variant curl openssl --triplet=${{ matrix.vcpkg_triplet }} - if: startsWith(matrix.os, 'windows') + if: startsWith(matrix.os, 'windows') && !matrix.msys2_env - uses: ilammy/msvc-dev-cmd@v1 with: arch: ${{ matrix.arch }} - if: startsWith(matrix.os, 'windows') + if: startsWith(matrix.os, 'windows') && !matrix.msys2_env - name: Set up build environment (Android) run: | @@ -134,18 +135,39 @@ jobs: if: matrix.os == 'android' - uses: mozilla-actions/sccache-action@v0.0.10 - if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'ubuntu') + if: (startsWith(matrix.os, 'windows') && !matrix.msys2_env) || startsWith(matrix.os, 'ubuntu') with: disable_annotations: true + - name: Set up MSYS2 (Windows ARM) + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.msys2_env }} + update: true + cache: true + install: | + git + pacboy: | + clang:p + ccache:p + cmake:p + lld:p + ninja:p + boost:p + openssl:p + curl:p + zlib:p + qt6-base:p + qt6-svg:p + qt6-tools:p + if: matrix.msys2_env + - name: Restore Gradle Cache uses: actions/cache@v5 with: path: '~/.gradle/' - key: ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }}-${{ hashFiles('android/**/*.xml') }}-${{ hashFiles('android/**.java') }} + key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle', 'android/**/*.properties', 'android/**/*.xml', 'android/**/*.java', 'android/**/*.kt') }} restore-keys: | - ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }}-${{ hashFiles('android/**/*.xml') }}- - ${{ runner.os }}-gradle-${{ hashFiles('**/build.gradle') }}- ${{ runner.os }}-gradle- if: matrix.os == 'android' @@ -162,11 +184,42 @@ jobs: run: ccache -z if: startsWith(matrix.os, 'macos') + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: '6.8.2' + cache: true + if: matrix.os != 'android' && !matrix.msys2_env + - name: Build Vita3K run: | cmake ${{ matrix.extra_cmake_args }} --preset ${{ matrix.cmake_preset }} cmake --build build/${{ matrix.cmake_preset }} --config ${{ env.BUILD_CONFIG }} - if: matrix.os != 'android' + if: matrix.os != 'android' && !matrix.msys2_env + + - name: Build Vita3K (MSYS2) + shell: msys2 {0} + run: | + export CCACHE_DIR=$(cygpath -u "C:/msys2-ccache") + cmake -G "Ninja Multi-Config" -B build/${{ matrix.cmake_preset }} \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + cmake --build build/${{ matrix.cmake_preset }} --config ${{ env.BUILD_CONFIG }} + if: matrix.msys2_env + + - name: Deploy MSYS2 runtime libraries + shell: msys2 {0} + run: | + BINDIR="build/${{ matrix.cmake_preset }}/bin/${{ env.BUILD_CONFIG }}" + MSYS2_BINDIR="/${MSYSTEM,,}/bin" + for dll in libc++.dll libunwind.dll; do + if [ -f "$MSYS2_BINDIR/$dll" ]; then + cp "$MSYS2_BINDIR/$dll" "$BINDIR/" + fi + done + if: matrix.msys2_env - name: Print ccache statistics run: ccache -s @@ -174,7 +227,7 @@ jobs: - name: Print sccache statistics run: sccache --show-stats - if: startsWith(matrix.os, 'windows') || startsWith(matrix.os, 'ubuntu') + if: (startsWith(matrix.os, 'windows') && !matrix.msys2_env) || startsWith(matrix.os, 'ubuntu') - name: Build Vita3K (Android) env: @@ -185,23 +238,26 @@ jobs: JVM_OPTS: -Xmx6G VCPKG_ROOT: /usr/local/share/vcpkg run: | - cp -r data android/assets - cp -r lang android/assets - cp -r vita3k/shaders-builtin android/assets - export JAVA_HOME=$(echo $JAVA_HOME_17_X64) + mkdir -p android/app/assets + rm -rf android/app/assets/data android/app/assets/shaders-builtin + cp -r data android/app/assets/data + cp -r vita3k/shaders-builtin android/app/assets/shaders-builtin + export JAVA_HOME="$JAVA_HOME_17_X64" + chmod +x android/gradlew if [ -e "$SIGNING_STORE_PATH" ]; then BUILD_TYPE=Release else BUILD_TYPE=Reldebug fi - ./gradlew --stacktrace --configuration-cache --build-cache --parallel --configure-on-demand assemble${BUILD_TYPE} - echo "APK_PATH=android/build/outputs/apk/${BUILD_TYPE,,}/android-${BUILD_TYPE,,}.apk" >> $GITHUB_ENV + cd android + ./gradlew --stacktrace --configuration-cache --build-cache --parallel --configure-on-demand :app:assemble${BUILD_TYPE} + echo "APK_PATH=android/app/build/outputs/apk/${BUILD_TYPE,,}/app-${BUILD_TYPE,,}.apk" >> $GITHUB_ENV if: matrix.os == 'android' - name: Run tests working-directory: build/${{ matrix.cmake_preset }} run: ctest --build-config ${{ env.BUILD_CONFIG }} --output-on-failure - if: matrix.os != 'android' + if: matrix.os != 'android' && !matrix.msys2_env - name: Compute git short SHA shell: bash diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3df6b5053..9838ab4fe 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -56,6 +56,13 @@ jobs: with: disable_annotations: true + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: '6.8.2' + cache: true + install-deps: 'true' + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.gitignore b/.gitignore index 52472407e..d5107b2f9 100755 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,9 @@ proguard/ # Android Studio captures folder captures/ +# Kotlin +.kotlin/ + # IntelliJ *.iml .idea/ @@ -64,9 +67,6 @@ captures/ # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild -# CXX compile cache -android/.cxx - # Google Services (e.g. APIs or Firebase) google-services.json diff --git a/.gitmodules b/.gitmodules index 57cbabaa3..fa35d9cb1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -23,12 +23,6 @@ [submodule "external/googletest"] path = external/googletest url = https://github.com/google/googletest -[submodule "external/imgui"] - path = external/imgui - url = https://github.com/ocornut/imgui -[submodule "external/imgui_club"] - path = external/imgui_club - url = https://github.com/ocornut/imgui_club [submodule "external/libfat16"] path = external/libfat16 url = https://github.com/Vita3K/libfat16 diff --git a/CMakeLists.txt b/CMakeLists.txt index b1336cea2..92849e814 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,7 +51,6 @@ else() option(BUILD_APPIMAGE "Build an AppImage." OFF) endif() -option(USE_VITA3K_UPDATE "Build Vita3K with updater." ON) option(FORCE_BUILD_OPENSSL_MAC OFF) if("${CMAKE_CXX_COMPILER_LAUNCHER}" STREQUAL "") diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 000000000..d315dfcaf --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,25 @@ +# Build artifacts +.gradle/ +build/ +bin/ +gen/ +out/ +.externalNativeBuild/ +.cxx/ +.kotlin/ + +# Local configuration +local.properties + +# IDE specific +.idea/ +*.iml +*.ipr +*.iws +.navigation/ +captures/ +.cache/ + +# System files +.DS_Store +Thumbs.db diff --git a/android/assets/.gitignore b/android/app/assets/.gitignore similarity index 100% rename from android/assets/.gitignore rename to android/app/assets/.gitignore diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 000000000..777a6f651 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,139 @@ +apply plugin: 'com.android.application' +apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'org.jetbrains.kotlin.plugin.compose' + +def signingStorePath = System.getenv("SIGNING_STORE_PATH") +def signingStorePassword = System.getenv("SIGNING_STORE_PASSWORD") +def signingKeyAlias = System.getenv("SIGNING_KEY_ALIAS") +def signingKeyPassword = System.getenv("SIGNING_KEY_PASSWORD") +def hasCiSigning = signingStorePath && signingStorePassword && signingKeyAlias && signingKeyPassword + +android { + namespace "org.vita3k.emulator" + compileSdk 35 + ndkVersion "27.3.13750724" + buildFeatures { + buildConfig true + compose true + } + + defaultConfig { + applicationId "org.vita3k.emulator" + minSdk 28 + targetSdk 35 + versionCode 21 + versionName "0.2.1" + + externalNativeBuild { + cmake { + abiFilters.addAll( 'arm64-v8a' ) + arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" // required until NDK r28 + } + } + } + + signingConfigs { + ci { + if (hasCiSigning) { + storeFile file(signingStorePath) + storePassword signingStorePassword + keyAlias signingKeyAlias + keyPassword signingKeyPassword + } + } + } + + buildTypes { + debug { + debuggable true + jniDebuggable true + applicationIdSuffix '.debug' + } + + reldebug { + debuggable true + jniDebuggable true + externalNativeBuild { + cmake { + arguments "-DCMAKE_BUILD_TYPE=RelWithDebInfo" + } + } + minifyEnabled false + shrinkResources false + applicationIdSuffix '.debug' + signingConfig = signingConfigs.debug + } + + release { + debuggable false + jniDebuggable false + externalNativeBuild { + cmake { + arguments "-DCMAKE_BUILD_TYPE=Release" + } + } + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig = hasCiSigning ? signingConfigs.ci : signingConfigs.debug + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + + sourceSets { + main { + jniLibs.srcDirs = ['../prebuilt'] + assets { + srcDirs 'assets' + } + java { + srcDirs += ['../../external/sdl/android-project/app/src/main/java'] + } + } + } + + externalNativeBuild { + cmake { + path '../../CMakeLists.txt' + version '3.22.1+' + } + } + + lint { + abortOnError false + checkReleaseBuilds false + } + packagingOptions { + jniLibs { + useLegacyPackaging true + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation "androidx.core:core:1.12.0" + implementation 'androidx.core:core-google-shortcuts:1.1.0' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.getkeepsafe.relinker:relinker:1.4.5' + + implementation platform('androidx.compose:compose-bom:2025.04.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.material:material-icons-extended' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.activity:activity-compose:1.9.3' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.7' + implementation 'androidx.navigation:navigation-compose:2.8.5' + implementation 'io.coil-kt:coil-compose:2.7.0' + debugImplementation 'androidx.compose.ui:ui-tooling' +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..220226dbe --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,11 @@ +# this is necessary so that native functions can call Java ones using JNI +-keep class org.libsdl.app** { *; } +-keep class org.vita3k.emulator.NativeLib { *; } +-keep class org.vita3k.emulator.Emulator { *; } +-keep class org.vita3k.emulator.EmuSurface { *; } +-keep class org.vita3k.emulator.overlay.** { *; } +-keep class org.vita3k.emulator.data.EmulatorConfig { *; } +-keep class org.vita3k.emulator.data.NativeAppInfo { *; } +-keep class org.vita3k.emulator.data.NativeImeState { *; } +-keep class org.vita3k.emulator.data.NativeUser { *; } +-keep interface org.vita3k.emulator.data.InstallCallback { *; } diff --git a/android/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml similarity index 73% rename from android/src/main/AndroidManifest.xml rename to android/app/src/main/AndroidManifest.xml index c11e183d5..080e2f338 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -61,11 +61,6 @@ android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:minSdkVersion="30" /> - - - @@ -73,6 +68,9 @@ + + + @@ -90,7 +88,8 @@ The requestLegacyExternalStorage parameter only has an effect when running on Android <= 10 --> - - - - + - + + + + + + - - + - - - - - - - - - - - + android:foregroundServiceType="dataSync" /> diff --git a/android/src/main/java/org/vita3k/emulator/EmuSurface.java b/android/app/src/main/java/org/vita3k/emulator/EmuSurface.java similarity index 99% rename from android/src/main/java/org/vita3k/emulator/EmuSurface.java rename to android/app/src/main/java/org/vita3k/emulator/EmuSurface.java index 13cb2622e..4f2ff83b3 100644 --- a/android/src/main/java/org/vita3k/emulator/EmuSurface.java +++ b/android/app/src/main/java/org/vita3k/emulator/EmuSurface.java @@ -44,5 +44,4 @@ public class EmuSurface extends SDLSurface { } public native void setSurfaceStatus(boolean surface_present); - } diff --git a/android/app/src/main/java/org/vita3k/emulator/Emulator.java b/android/app/src/main/java/org/vita3k/emulator/Emulator.java new file mode 100644 index 000000000..baf233ce8 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/Emulator.java @@ -0,0 +1,1278 @@ +package org.vita3k.emulator; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.hardware.input.InputManager; +import android.os.Build; +import android.os.Bundle; +import android.os.SystemClock; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Surface; +import android.view.ViewGroup; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.RelativeLayout; +import android.widget.Toast; + +import androidx.annotation.Keep; +import androidx.compose.ui.platform.ComposeView; +import androidx.core.app.ActivityCompat; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStore; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.lifecycle.ViewTreeLifecycleOwner; +import androidx.savedstate.SavedStateRegistry; +import androidx.savedstate.SavedStateRegistryController; +import androidx.savedstate.SavedStateRegistryOwner; +import androidx.savedstate.ViewTreeSavedStateRegistryOwner; +import androidx.lifecycle.ViewTreeViewModelStoreOwner; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import org.libsdl.app.SDLActivity; +import org.libsdl.app.SDLDummyEdit; +import org.libsdl.app.SDLSurface; +import org.vita3k.emulator.data.AppStorage; +import org.vita3k.emulator.data.NativeImeState; +import org.vita3k.emulator.overlay.InputOverlay; +import org.vita3k.emulator.ui.screens.emulation.NativeImeOverlayHost; +import org.vita3k.emulator.ui.screens.emulation.EmulationPauseMenuHost; +import org.vita3k.emulator.ui.viewmodel.EmulationSessionViewModel; +import org.vita3k.emulator.ui.viewmodel.SettingsViewModel; + +public class Emulator extends SDLActivity + implements InputManager.InputDeviceListener, LifecycleOwner, SavedStateRegistryOwner, ViewModelStoreOwner +{ + private static final String TAG = "Vita3K"; + private static final long IME_RESTORE_DELAY_MS = 250L; + public static final String EXTRA_TITLE_ID = "title_id"; + public static final String EXTRA_GAME_TITLE = "game_title"; + private static final String APP_RESTART_PARAMETERS = "AppStartParameters"; + static final int FILE_DIALOG_CODE = 545; + static final int FOLDER_DIALOG_CODE = 546; + static final int FOLDER_ACCESS_SETTINGS_CODE = 547; + static final int FOLDER_ACCESS_PERMISSION_CODE = 548; + public interface ImagePathResultCallback { + void onResult(String path); + } + private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this); + private final SavedStateRegistryController savedStateRegistryController = + SavedStateRegistryController.create(this); + private final ViewModelStore composeViewModelStore = new ViewModelStore(); + private String currentGameId = ""; + private EmuSurface mSurface; + private ComposeView imeOverlayView; + private ComposeView pauseMenuView; + private EmulationSessionViewModel sessionViewModel; + private SettingsViewModel pauseSettingsViewModel; + private SettingsViewModel pauseGlobalSettingsViewModel; + private InputManager inputManager; + private boolean nativeKeyboardRequested; + private boolean imeDismissedByUser; + private boolean imeVisible; + private boolean imeWasVisibleSinceRequest; + private boolean suppressImeHiddenHandler; + private boolean restoreImeAfterPauseMenu; + private long lastImeCompletionUptimeMs; + private RelativeLayout.LayoutParams lastTextInputLayoutParams; + private int lastTextInputType; + private boolean hasLastTextInputState; + private ImagePathResultCallback pendingImagePathCallback; + + public static Intent createLaunchIntent(Context context, String titleId, String gameTitle) { + Intent intent = new Intent(context, Emulator.class); + if (titleId != null && !titleId.isEmpty()) { + intent.putExtra(EXTRA_TITLE_ID, titleId); + } + if (gameTitle != null && !gameTitle.isEmpty()) { + intent.putExtra(EXTRA_GAME_TITLE, gameTitle); + } + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } + + public InputOverlay getmOverlay() { + return mSurface != null ? mSurface.getmOverlay() : null; + } + + @Keep + public void setCurrentGameId(String gameId){ + runOnUiThread(() -> { + currentGameId = gameId != null ? gameId : ""; + if (sessionViewModel != null) { + String runningAppTitle = ""; + try { + runningAppTitle = NativeLib.INSTANCE.getRunningAppTitle(); + } catch (Throwable ignored) { + } + sessionViewModel.updateRunningApp(currentGameId, runningAppTitle); + } + releaseControllerOverlayInputs(); + refreshUiState(true); + }); + } + + /** + * This method is called by SDL before loading the native shared libraries. + * It can be overridden to provide names of shared libraries to be loaded. + * The default implementation returns the defaults. It never returns null. + * An array returned by a new implementation must at least contain "SDL2". + * Also keep in mind that the order the libraries are loaded may matter. + * + * @return names of shared libraries to be loaded (e.g. "SDL3", "main"). + */ + @Override + protected String[] getLibraries() { + return new String[] { "Vita3K" }; + } + + @Override + protected SDLSurface createSDLSurface(Context context) { + mSurface = new EmuSurface(context); + mSurface.post(() -> { + if (mSurface.getParent() instanceof ViewGroup) { + InputOverlay overlay = getmOverlay(); + if (overlay != null && overlay.getParent() == null) { + ((ViewGroup) mSurface.getParent()).addView(overlay); + } + refreshUiState(true); + } + }); + return mSurface; + } + + @Override + protected String[] getArguments() { + Intent intent = getIntent(); + + // Check for restart parameters first (used by in-process relaunches) + String[] args = intent.getStringArrayExtra(APP_RESTART_PARAMETERS); + if (args != null && args.length > 0) + return args; + + // Check for title_id from MainActivity launch + String titleId = intent.getStringExtra(EXTRA_TITLE_ID); + if (titleId != null && !titleId.isEmpty()) + return new String[]{"-r", titleId}; + + return new String[]{}; + } + + @Override + protected void onNewIntent(Intent intent){ + super.onNewIntent(intent); + final Intent currentIntent = getIntent(); + setIntent(intent); + if (!hasLaunchRequest(intent)) + return; + + if (isSameLaunchRequest(currentIntent, intent) && NativeLib.INSTANCE.isAppRunning()) + return; + + requestRelaunchFromIntent(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (!ensureNativeSessionInitialized()) { + finish(); + return; + } + savedStateRegistryController.performAttach(); + savedStateRegistryController.performRestore(savedInstanceState); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + prepareComposeHostTree(); + setWindowStyle(true); + applyImmersiveMode(); + refreshNativeDisplayRotation(); + + currentGameId = resolveCurrentTitleId(getIntent()); + String gameTitle = getIntent().getStringExtra(EXTRA_GAME_TITLE); + sessionViewModel = new EmulationSessionViewModel(getApplication()); + sessionViewModel.initialize(currentGameId, gameTitle); + inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); + if (inputManager != null) { + inputManager.registerInputDeviceListener(this, null); + } + updateControllerConnectionState(); + syncOverlayFromSession(); + installImeOverlay(); + installPauseMenu(); + installKeyboardVisibilityListener(); + refreshUiState(false); + } + + @Override + protected void onResume() { + super.onResume(); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + refreshUiState(false); + refreshNativeDisplayRotation(); + updateControllerConnectionState(); + resumeFromBackgroundIfNeeded(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + refreshNativeDisplayRotation(); + } + + @Override + protected void onStart() { + super.onStart(); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + } + + @Override + protected void onPause() { + suspendForBackgroundIfNeeded(); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); + super.onPause(); + } + + @Override + protected void onStop() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + super.onStop(); + } + + @Override + protected void onDestroy() { + if (inputManager != null) { + inputManager.unregisterInputDeviceListener(this); + } + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + composeViewModelStore.clear(); + super.onDestroy(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + savedStateRegistryController.performSave(outState); + super.onSaveInstanceState(outState); + } + + @Override + public void onBackPressed() { + if (sessionViewModel != null && sessionViewModel.handleBackPressed(this)) + return; + + super.onBackPressed(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + refreshUiState(false); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && nativeKeyboardRequested) { + if (event.getAction() == KeyEvent.ACTION_UP && event.getRepeatCount() == 0) { + dismissImeFromKeyboard(mTextEdit); + } + return true; + } + + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + if (event.getAction() == KeyEvent.ACTION_UP + && event.getRepeatCount() == 0 + && sessionViewModel != null + && sessionViewModel.handleBackPressed(this)) { + return true; + } + return true; + } + + if (event.getKeyCode() == KeyEvent.KEYCODE_BUTTON_MODE + && event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0) { + openPauseMenuFromController(); + return true; + } + + return super.dispatchKeyEvent(event); + } + + @Keep + public void restartApp(String app_path, String exec_path, String exec_args){ + final String[] guestArgs = exec_args != null && !exec_args.isEmpty() + ? new String[]{exec_args} + : new String[0]; + requestNativeRelaunch(app_path, exec_path, guestArgs, !exec_path.isEmpty()); + } + + @Keep + public void setControllerOverlayState(int overlay_mask, boolean edit, boolean reset){ + InputOverlay overlay = getmOverlay(); + if (overlay == null) + return; + + overlay.setState(overlay_mask); + overlay.setIsInEditMode(edit); + + if(reset) + overlay.resetButtonPlacement(); + } + + private int mSavedOverlayMask = 0; + + @Keep + public void setKeyboardActive(boolean active) { + runOnUiThread(() -> { + nativeKeyboardRequested = active; + if (active) { + imeDismissedByUser = false; + restoreImeAfterPauseMenu = false; + suppressImeHiddenHandler = false; + imeWasVisibleSinceRequest = imeVisible; + } else { + imeDismissedByUser = false; + restoreImeAfterPauseMenu = false; + suppressImeHiddenHandler = false; + imeWasVisibleSinceRequest = false; + } + applyKeyboardOverlayState(active); + if (active) { + scheduleVitaTextEditSwap(); + } + if (active) { + ensureOverlayUiOrder(); + } + }); + } + + public void updateNativeImeState(boolean sceImeActive, + boolean dialogActive, + String text, + int preeditStart, + int preeditLength, + int caretIndex, + boolean multiline, + String enterLabel) { + runOnUiThread(() -> { + if (sessionViewModel == null) { + return; + } + + sessionViewModel.updateImeState(new NativeImeState( + sceImeActive, + dialogActive, + text != null ? text : "", + preeditStart, + preeditLength, + caretIndex, + multiline, + enterLabel != null ? enterLabel : "")); + }); + } + + public void clearNativeImeState() { + runOnUiThread(() -> { + if (sessionViewModel != null) { + sessionViewModel.updateImeState(null); + } + }); + } + + @Keep + public void setControllerOverlayScale(float scale){ + InputOverlay overlay = getmOverlay(); + if (overlay != null) + overlay.setScale(scale); + } + + @Keep + public void setControllerOverlayOpacity(int opacity){ + InputOverlay overlay = getmOverlay(); + if (overlay != null) + overlay.setOpacity(opacity); + } + + public void releaseControllerOverlayInputs() { + InputOverlay overlay = getmOverlay(); + if (overlay != null) + overlay.releaseAllInputs(); + } + + public void requestNativeQuit() { + runOnUiThread(() -> { + try { + NativeLib.INSTANCE.requestAppQuit(); + } catch (Throwable ignored) { + } + if (!isFinishing()) { + finish(); + } + }); + } + + public void ensurePauseMenuOnTop() { + ensureOverlayUiOrder(); + } + + public void ensureOverlayUiOrder() { + if (imeOverlayView != null) { + imeOverlayView.bringToFront(); + } + if (pauseMenuView != null) { + pauseMenuView.bringToFront(); + } + } + + public void prepareImeForPauseMenu() { + runOnUiThread(() -> { + if (nativeKeyboardRequested && !imeDismissedByUser) { + restoreImeAfterPauseMenu = true; + suppressImeHiddenHandler = true; + } + }); + } + + public void restoreImeAfterPauseMenu() { + runOnUiThread(() -> { + if (!restoreImeAfterPauseMenu) { + suppressImeHiddenHandler = false; + return; + } + + restoreImeAfterPauseMenu = false; + suppressImeHiddenHandler = false; + if (!nativeKeyboardRequested || imeDismissedByUser || imeVisible || isFinishing()) { + return; + } + + ensureOverlayUiOrder(); + View anchor = mSurface != null ? mSurface : getWindow().getDecorView(); + anchor.postDelayed(() -> { + if (nativeKeyboardRequested + && !imeDismissedByUser + && !imeVisible + && sessionViewModel != null + && !sessionViewModel.getUiState().getShowMenu() + && !sessionViewModel.getUiState().isEditingControls()) { + restoreVitaTextInput(); + } + }, 32L); + }); + } + + public void completeImeFromKeyboard(View source) { + runOnUiThread(() -> { + if (!nativeKeyboardRequested || imeDismissedByUser) { + return; + } + + long now = SystemClock.uptimeMillis(); + if (now - lastImeCompletionUptimeMs < 150L) { + return; + } + lastImeCompletionUptimeMs = now; + + imeDismissedByUser = true; + restoreImeAfterPauseMenu = false; + suppressImeHiddenHandler = true; + applyKeyboardOverlayState(false); + + View target = source != null ? source : (mSurface != null ? mSurface : getWindow().getDecorView()); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(target.getWindowToken(), 0); + } + target.clearFocus(); + if (mTextEdit instanceof VitaTextEdit) { + ((VitaTextEdit) mTextEdit).clearConnectionState(); + } + if (mSurface != null) { + mSurface.requestFocus(); + } + + try { + NativeLib.INSTANCE.submitIme(); + } catch (Throwable ignored) { + } + }); + } + + public void dismissImeFromKeyboard(View source) { + runOnUiThread(() -> { + if (!nativeKeyboardRequested || isFinishing()) { + return; + } + + long now = SystemClock.uptimeMillis(); + if (now - lastImeCompletionUptimeMs < 150L) { + return; + } + lastImeCompletionUptimeMs = now; + + View target = source != null ? source : (mSurface != null ? mSurface : getWindow().getDecorView()); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(target.getWindowToken(), 0); + } + target.clearFocus(); + if (mTextEdit instanceof VitaTextEdit) { + ((VitaTextEdit) mTextEdit).clearConnectionState(); + } + if (mSurface != null) { + mSurface.requestFocus(); + } + + NativeImeState imeState = sessionViewModel != null ? sessionViewModel.getImeState() : null; + boolean dialogActive = imeState != null && imeState.getDialogActive(); + + boolean dismissedInNative = false; + try { + dismissedInNative = NativeLib.INSTANCE.dismissIme(); + } catch (Throwable ignored) { + } + + restoreImeAfterPauseMenu = false; + suppressImeHiddenHandler = true; + + if (dismissedInNative && dialogActive) { + imeDismissedByUser = true; + applyKeyboardOverlayState(false); + return; + } + + imeDismissedByUser = false; + + if (sessionViewModel != null + && (sessionViewModel.getUiState().getShowMenu() + || sessionViewModel.getUiState().isEditingControls())) { + restoreImeAfterPauseMenu = true; + return; + } + + scheduleImeRestoreCheck(target); + }); + } + + private void scheduleImeRestoreCheck(View anchor) { + View target = anchor != null ? anchor : (mSurface != null ? mSurface : getWindow().getDecorView()); + ensureOverlayUiOrder(); + target.postDelayed(() -> { + NativeImeState imeState = sessionViewModel != null ? sessionViewModel.getImeState() : null; + boolean imeStillActive = imeState != null + && (imeState.getSceImeActive() || imeState.getDialogActive()); + + if (!nativeKeyboardRequested + || !imeStillActive + || imeVisible + || isFinishing() + || (sessionViewModel != null + && (sessionViewModel.getUiState().getShowMenu() + || sessionViewModel.getUiState().isEditingControls()))) { + return; + } + + restoreVitaTextInput(); + ensureOverlayUiOrder(); + }, IME_RESTORE_DELAY_MS); + } + + public void openPauseMenuFromController() { + runOnUiThread(() -> { + if (sessionViewModel == null || isFinishing() || sessionViewModel.getUiState().getShowMenu() + || sessionViewModel.getUiState().isEditingControls()) { + return; + } + + ensurePauseMenuOnTop(); + sessionViewModel.openMenu(this, true); + }); + } + + @Keep + public boolean createShortcut(String game_id, String game_name){ + if(!ShortcutManagerCompat.isRequestPinShortcutSupported(getContext())) + return false; + + // first look at the icon, its location should always be the same + File src_icon = new File(getExternalFilesDir(null), "cache/icons/" + game_id + ".png"); + Bitmap icon; + if(src_icon.exists()) + icon = BitmapFactory.decodeFile(src_icon.getPath()); + else + icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); + + // intent to directly start the game + Intent game_intent = createLaunchIntent(getContext(), game_id, game_name); + + // now create the pinned shortcut + ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(getContext(), game_id) + .setShortLabel(game_name) + .setLongLabel(game_name) + .setIcon(IconCompat.createWithBitmap(icon)) + .setIntent(game_intent) + .build(); + ShortcutManagerCompat.requestPinShortcut(getContext(), shortcut, null); + + return true; + } + + @Keep + public void showFileDialog() { + if (!StorageAccess.hasStorageAccess(this)) { + requestFolderAccess(); + return; + } + + startActivityForResult( + Intent.createChooser( + StorageAccess.createFilePickerIntent(new String[]{"*/*"}), + "Choose a file"), + FILE_DIALOG_CODE); + } + + @Keep + public void showFolderDialog() { + if (!StorageAccess.hasStorageAccess(this)) { + requestFolderAccess(); + return; + } + + startActivityForResult( + Intent.createChooser(StorageAccess.createFolderPickerIntent(), "Choose a folder"), + FOLDER_DIALOG_CODE); + } + + public void requestImagePath(ImagePathResultCallback onResult) { + pendingImagePathCallback = onResult; + if (!StorageAccess.hasStorageAccess(this)) { + requestFolderAccess(); + return; + } + + launchImagePicker(); + } + + private void launchImagePicker() { + startActivityForResult( + Intent.createChooser( + StorageAccess.createFilePickerIntent(new String[]{"image/*"}), + "Choose an image"), + FILE_DIALOG_CODE); + } + + private void dispatchPendingImagePath(String path) { + ImagePathResultCallback callback = pendingImagePathCallback; + pendingImagePathCallback = null; + if (callback != null) { + callback.onResult(path); + } + } + + @Keep + public int getNativeDisplayRotation() { + // Returns the device's default display rotation (0, 90, 180, or 270 degrees) + return getWindowManager().getDefaultDisplay().getRotation(); + } + + private void refreshNativeDisplayRotation() { + try { + setNativeDisplayRotation(getNativeDisplayRotation()); + } catch (Throwable ignored) { + } + } + + private native void setNativeDisplayRotation(int rotation); + + public native void filedialogReturn(String result_path); + + private void scheduleVitaTextEditSwap() { + final View anchor = mSurface != null ? mSurface : getWindow().getDecorView(); + anchor.post(() -> { + if (mTextEdit == null && nativeKeyboardRequested) { + anchor.post(this::ensureVitaTextEdit); + return; + } + ensureVitaTextEdit(); + }); + } + + private void ensureVitaTextEdit() { + if (!(mLayout instanceof RelativeLayout) || mTextEdit == null) { + return; + } + + captureTextInputState(mTextEdit); + if (mTextEdit instanceof VitaTextEdit) { + return; + } + + final RelativeLayout.LayoutParams params = + lastTextInputLayoutParams != null + ? new RelativeLayout.LayoutParams(lastTextInputLayoutParams) + : new RelativeLayout.LayoutParams(mTextEdit.getLayoutParams()); + final boolean visible = mTextEdit.getVisibility() == View.VISIBLE; + final boolean focused = mTextEdit.hasFocus(); + + mLayout.removeView(mTextEdit); + final VitaTextEdit replacement = new VitaTextEdit(this); + replacement.setInputType(lastTextInputType); + mTextEdit = replacement; + mLayout.addView(mTextEdit, params); + + if (visible) { + mTextEdit.setVisibility(View.VISIBLE); + if (focused || nativeKeyboardRequested) { + mTextEdit.requestFocus(); + showSoftKeyboard(mTextEdit); + } + } + } + + private void captureTextInputState(SDLDummyEdit textEdit) { + if (textEdit == null) { + return; + } + + if (textEdit.getLayoutParams() instanceof RelativeLayout.LayoutParams) { + lastTextInputLayoutParams = new RelativeLayout.LayoutParams( + (RelativeLayout.LayoutParams) textEdit.getLayoutParams()); + } + lastTextInputType = VitaTextEdit.getInputTypeValue(textEdit); + hasLastTextInputState = true; + } + + private void restoreVitaTextInput() { + if (!hasLastTextInputState || !(mLayout instanceof RelativeLayout) || lastTextInputLayoutParams == null) { + return; + } + + if (mTextEdit == null) { + final VitaTextEdit replacement = new VitaTextEdit(this); + replacement.setInputType(lastTextInputType); + mTextEdit = replacement; + mLayout.addView(mTextEdit, new RelativeLayout.LayoutParams(lastTextInputLayoutParams)); + } else { + ensureVitaTextEdit(); + mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(lastTextInputLayoutParams)); + mTextEdit.setInputType(lastTextInputType); + } + + mTextEdit.setVisibility(View.VISIBLE); + mTextEdit.requestFocus(); + showSoftKeyboard(mTextEdit); + } + + private void showSoftKeyboard(View view) { + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(view, 0); + } + ensureOverlayUiOrder(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + switch (requestCode) { + case FOLDER_ACCESS_SETTINGS_CODE: + if (StorageAccess.hasStorageAccess(this)) { + if (pendingImagePathCallback != null) { + launchImagePicker(); + } else { + showFolderDialog(); + } + } else { + if (pendingImagePathCallback != null) { + dispatchPendingImagePath(null); + } else { + filedialogReturn(""); + } + } + break; + case FILE_DIALOG_CODE: + if (pendingImagePathCallback != null) { + final String resolvedPath = resultCode == RESULT_OK && data != null && data.getData() != null + ? StorageAccess.resolveUriToPath(this, data.getData()) + : null; + dispatchPendingImagePath(resolvedPath); + break; + } + if (resultCode != RESULT_OK || data == null || data.getData() == null) { + filedialogReturn(""); + break; + } + { + final String resolvedPath = StorageAccess.resolveUriToPath(this, data.getData()); + filedialogReturn(resolvedPath != null ? resolvedPath : ""); + } + break; + case FOLDER_DIALOG_CODE: { + final String selectedPath = resultCode == RESULT_OK && data != null && data.getData() != null + ? StorageAccess.resolveTreeUriToPath(this, data.getData()) + : null; + filedialogReturn(selectedPath != null ? selectedPath : ""); + break; + } + default: + break; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode != FOLDER_ACCESS_PERMISSION_CODE) { + return; + } + + if (StorageAccess.hasStorageAccess(this)) { + if (pendingImagePathCallback != null) { + launchImagePicker(); + } else { + showFolderDialog(); + } + } else { + if (pendingImagePathCallback != null) { + dispatchPendingImagePath(null); + } else { + filedialogReturn(""); + } + } + } + + private void requestFolderAccess() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + startActivityForResult( + StorageAccess.createManageAllFilesIntent(this), + FOLDER_ACCESS_SETTINGS_CODE); + return; + } + + ActivityCompat.requestPermissions( + this, + StorageAccess.missingStoragePermissions(this), + FOLDER_ACCESS_PERMISSION_CODE); + } + + private void applyImmersiveMode() { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + WindowInsetsControllerCompat controller = + ViewCompat.getWindowInsetsController(getWindow().getDecorView()); + if (controller != null) { + controller.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + controller.hide(WindowInsetsCompat.Type.systemBars()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WindowManager.LayoutParams attributes = getWindow().getAttributes(); + if (attributes.layoutInDisplayCutoutMode + != WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) { + attributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(attributes); + } + } + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void installPauseMenu() { + if (!(SDLActivity.getContentView() instanceof ViewGroup)) + return; + + ViewGroup contentView = (ViewGroup) SDLActivity.getContentView(); + prepareComposeHostView(contentView); + pauseMenuView = new ComposeView(this); + prepareComposeOverlayView(pauseMenuView); + pauseMenuView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + pauseMenuView.setVisibility(View.GONE); + contentView.addView(pauseMenuView); + ensureOverlayUiOrder(); + EmulationPauseMenuHost.attach( + this, + pauseMenuView, + sessionViewModel, + getPauseSettingsViewModel(), + getPauseGlobalSettingsViewModel()); + } + + private void installImeOverlay() { + if (!(SDLActivity.getContentView() instanceof ViewGroup)) + return; + + ViewGroup contentView = (ViewGroup) SDLActivity.getContentView(); + prepareComposeHostView(contentView); + imeOverlayView = new ComposeView(this); + prepareComposeOverlayView(imeOverlayView); + if (contentView instanceof RelativeLayout) { + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.addRule(RelativeLayout.ALIGN_PARENT_TOP); + imeOverlayView.setLayoutParams(params); + } else { + imeOverlayView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + imeOverlayView.setVisibility(View.GONE); + contentView.addView(imeOverlayView); + NativeImeOverlayHost.attach(imeOverlayView, sessionViewModel); + ensureOverlayUiOrder(); + } + + private void installKeyboardVisibilityListener() { + View root = getWindow().getDecorView(); + ViewCompat.setOnApplyWindowInsetsListener(root, (view, insets) -> { + handleImeVisibilityChanged(insets.isVisible(WindowInsetsCompat.Type.ime())); + return insets; + }); + root.post(() -> { + WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(root); + handleImeVisibilityChanged(insets != null && insets.isVisible(WindowInsetsCompat.Type.ime())); + }); + } + + private void prepareComposeOverlayView(ComposeView composeView) { + ViewTreeLifecycleOwner.set(composeView, this); + ViewTreeSavedStateRegistryOwner.set(composeView, this); + ViewTreeViewModelStoreOwner.set(composeView, this); + } + + private void prepareComposeHostTree() { + prepareComposeHostView(getWindow().getDecorView()); + View contentView = SDLActivity.getContentView(); + if (contentView != null) { + prepareComposeHostView(contentView); + } + } + + private void prepareComposeHostView(View view) { + if (view == null) { + return; + } + ViewTreeLifecycleOwner.set(view, this); + ViewTreeSavedStateRegistryOwner.set(view, this); + ViewTreeViewModelStoreOwner.set(view, this); + } + + private SettingsViewModel getPauseSettingsViewModel() { + if (pauseSettingsViewModel == null) { + ViewModelProvider provider = new ViewModelProvider( + this, + ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())); + pauseSettingsViewModel = provider.get("pause-per-app-settings", SettingsViewModel.class); + } + return pauseSettingsViewModel; + } + + private SettingsViewModel getPauseGlobalSettingsViewModel() { + if (pauseGlobalSettingsViewModel == null) { + ViewModelProvider provider = new ViewModelProvider( + this, + ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())); + pauseGlobalSettingsViewModel = provider.get("pause-global-controller-settings", SettingsViewModel.class); + } + return pauseGlobalSettingsViewModel; + } + + @Override + public Lifecycle getLifecycle() { + return lifecycleRegistry; + } + + @Override + public SavedStateRegistry getSavedStateRegistry() { + return savedStateRegistryController.getSavedStateRegistry(); + } + + @Override + public ViewModelStore getViewModelStore() { + return composeViewModelStore; + } + + private void handleImeVisibilityChanged(boolean visible) { + if (imeVisible == visible) { + return; + } + + imeVisible = visible; + if (visible) { + imeWasVisibleSinceRequest = true; + ensureOverlayUiOrder(); + return; + } + + if (!nativeKeyboardRequested || imeDismissedByUser) { + return; + } + + if (suppressImeHiddenHandler) { + suppressImeHiddenHandler = false; + return; + } + + if (sessionViewModel != null + && (sessionViewModel.getUiState().getShowMenu() + || sessionViewModel.getUiState().isEditingControls())) { + restoreImeAfterPauseMenu = true; + return; + } + + if (imeWasVisibleSinceRequest) { + dismissImeFromKeyboard(mTextEdit); + } + } + + private void applyKeyboardOverlayState(boolean active) { + InputOverlay overlay = getmOverlay(); + if (overlay == null) { + return; + } + + if (active) { + mSavedOverlayMask = overlay.getOverlayMask(); + setControllerOverlayState(0, false, false); + } else { + setControllerOverlayState(mSavedOverlayMask, false, false); + } + } + + private boolean ensureNativeSessionInitialized() { + if (NativeLib.INSTANCE.isInitialized()) { + return true; + } + + final String storagePath = AppStorage.INSTANCE.storageRootPath(this); + final boolean initialized = NativeLib.INSTANCE.init(storagePath); + if (!initialized) { + Log.e(TAG, "Failed to initialize native session for direct emulator launch"); + Toast.makeText(this, "Failed to initialize Vita3K.", Toast.LENGTH_LONG).show(); + } + return initialized; + } + + private void suspendForBackgroundIfNeeded() { + if (isFinishing()) { + return; + } + + boolean running = false; + try { + running = NativeLib.INSTANCE.isAppRunning(); + } catch (Throwable ignored) { + } + if (!running) { + return; + } + + try { + NativeLib.INSTANCE.setPauseReasonEnabled(NativeLib.PAUSE_REASON_BACKGROUND, true); + } catch (Throwable ignored) { + } + } + + private void resumeFromBackgroundIfNeeded() { + final View anchor = mSurface != null ? mSurface : getWindow().getDecorView(); + anchor.post(() -> { + boolean running = false; + try { + running = NativeLib.INSTANCE.isAppRunning(); + } catch (Throwable ignored) { + } + if (!running) { + return; + } + try { + NativeLib.INSTANCE.setPauseReasonEnabled(NativeLib.PAUSE_REASON_BACKGROUND, false); + } catch (Throwable ignored) { + } + }); + } + + private boolean hasLaunchRequest(Intent intent) { + return intent != null + && (hasRestartParameters(intent) || !resolveCurrentTitleId(intent).isEmpty()); + } + + private boolean hasRestartParameters(Intent intent) { + String[] args = intent != null ? intent.getStringArrayExtra(APP_RESTART_PARAMETERS) : null; + return args != null && args.length > 0; + } + + private boolean isSameLaunchRequest(Intent currentIntent, Intent intent) { + final String[] currentArgs = currentIntent != null + ? currentIntent.getStringArrayExtra(APP_RESTART_PARAMETERS) + : null; + final String[] incomingArgs = intent.getStringArrayExtra(APP_RESTART_PARAMETERS); + if (!Arrays.equals(currentArgs, incomingArgs)) { + return false; + } + + return resolveCurrentTitleId(currentIntent).equals(resolveCurrentTitleId(intent)); + } + + private String resolveCurrentTitleId(Intent intent) { + if (intent == null) + return ""; + + String titleId = intent.getStringExtra(EXTRA_TITLE_ID); + if (titleId != null && !titleId.isEmpty()) + return titleId; + + String action = intent.getAction(); + if (action != null && action.startsWith("LAUNCH_")) + return action.substring(7); + + String[] args = intent.getStringArrayExtra(APP_RESTART_PARAMETERS); + if (args != null) { + for (int i = 0; i + 1 < args.length; i++) { + if ("-r".equals(args[i])) + return args[i + 1]; + } + } + + return ""; + } + + private void requestRelaunchFromIntent(Intent intent) { + final String[] args = intent != null ? intent.getStringArrayExtra(APP_RESTART_PARAMETERS) : null; + if (args != null && args.length > 0) { + final String titleId = resolveCurrentTitleId(intent); + final String selfPath = extractSelfPath(args); + requestNativeRelaunch(titleId, selfPath, extractGuestArgs(args), !selfPath.isEmpty()); + return; + } + + final String titleId = resolveCurrentTitleId(intent); + if (!titleId.isEmpty()) { + requestNativeRelaunch(titleId, "", new String[0], false); + } + } + + private void requestNativeRelaunch(String titleId, String selfPath, String[] args, boolean loadExecReason) { + try { + NativeLib.INSTANCE.requestAppRelaunch( + titleId != null ? titleId : "", + selfPath != null ? selfPath : "", + args != null ? args : new String[0], + loadExecReason); + } catch (Throwable error) { + Log.e(TAG, "Failed to request native relaunch", error); + } + } + + private String extractSelfPath(String[] args) { + if (args == null) { + return ""; + } + + for (int i = 0; i + 1 < args.length; i++) { + if ("--self".equals(args[i])) { + return args[i + 1]; + } + } + + return ""; + } + + private String[] extractGuestArgs(String[] args) { + if (args == null) { + return new String[0]; + } + + for (int i = 0; i + 1 < args.length; i++) { + if ("--app-args".equals(args[i])) { + return new String[]{args[i + 1]}; + } + } + + return new String[0]; + } + + private void updateControllerConnectionState() { + if (sessionViewModel != null) { + sessionViewModel.setControllerConnected(this, InputDeviceUtils.hasPhysicalGamepadConnected()); + } + } + + private void refreshUiState(boolean forceOverlayRebind) { + if (isFinishing()) { + return; + } + + setWindowStyle(true); + applyImmersiveMode(); + syncOverlayFromSession(forceOverlayRebind); + ensureOverlayUiOrder(); + + View decorView = getWindow().getDecorView(); + decorView.post(() -> { + if (!isFinishing()) { + applyImmersiveMode(); + ensureOverlayUiOrder(); + } + }); + } + + private void syncOverlayFromSession() { + syncOverlayFromSession(false); + } + + private void syncOverlayFromSession(boolean forceOverlayRebind) { + InputOverlay overlay = getmOverlay(); + if (overlay != null) { + overlay.setLayoutProfileId(currentGameId); + } + if (sessionViewModel != null) { + sessionViewModel.applyOverlayState(this); + } + if (nativeKeyboardRequested) { + applyKeyboardOverlayState(true); + } + if (forceOverlayRebind && overlay != null) { + overlay.rebindController(); + } + } + + @Override + public void onInputDeviceAdded(int deviceId) { + updateControllerConnectionState(); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + updateControllerConnectionState(); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + updateControllerConnectionState(); + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/InputDeviceUtils.kt b/android/app/src/main/java/org/vita3k/emulator/InputDeviceUtils.kt new file mode 100644 index 000000000..0ee144872 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/InputDeviceUtils.kt @@ -0,0 +1,61 @@ +package org.vita3k.emulator + +import android.view.InputDevice + +enum class ControllerFamily { + Standard, + Xbox, + Nintendo +} + +data class ConnectedGamepad( + val deviceId: Int, + val name: String, + val family: ControllerFamily +) + +object InputDeviceUtils { + @JvmStatic + fun hasPhysicalGamepadConnected(): Boolean { + return getPhysicalGamepads().isNotEmpty() + } + + @JvmStatic + fun getPhysicalGamepads(): List { + val controllers = mutableListOf() + val deviceIds = InputDevice.getDeviceIds() + for (deviceId in deviceIds) { + val device = InputDevice.getDevice(deviceId) ?: continue + if (device.isVirtual) { + continue + } + + val name = device.name.orEmpty() + if (name.startsWith("uinput-") || name.startsWith("gf_")) { + continue + } + + val sources = device.sources + val isGamepad = (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD || + (sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK + if (isGamepad) { + controllers += ConnectedGamepad( + deviceId = deviceId, + name = name, + family = inferControllerFamily(name) + ) + } + } + + return controllers.sortedBy { it.deviceId } + } + + private fun inferControllerFamily(name: String): ControllerFamily { + val normalized = name.lowercase() + return when { + normalized.contains("xbox") || normalized.contains("x-input") -> ControllerFamily.Xbox + normalized.contains("switch") || normalized.contains("joy-con") || normalized.contains("nintendo") -> ControllerFamily.Nintendo + else -> ControllerFamily.Standard + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/InstallForegroundService.kt b/android/app/src/main/java/org/vita3k/emulator/InstallForegroundService.kt new file mode 100644 index 000000000..bfb7461c1 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/InstallForegroundService.kt @@ -0,0 +1,371 @@ +package org.vita3k.emulator + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.os.PowerManager +import androidx.core.app.NotificationCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import org.vita3k.emulator.data.InstallRepository +import org.vita3k.emulator.ui.viewmodel.DeleteSourceOption +import org.vita3k.emulator.ui.viewmodel.InstallResult +import org.vita3k.emulator.ui.viewmodel.InstallResultStatus +import java.io.File + +class InstallForegroundService : Service() { + private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var wakeLock: PowerManager.WakeLock? = null + private var activeOperationId: Long = 0L + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val operationId = intent?.getLongExtra(InstallServiceController.EXTRA_OPERATION_ID, 0L) ?: 0L + val operationTypeName = intent?.getStringExtra(InstallServiceController.EXTRA_OPERATION_TYPE) + val operationType = operationTypeName?.let(InstallOperationType::valueOf) + if (operationId == 0L || operationType == null) { + stopSelfResult(startId) + return START_NOT_STICKY + } + + if (activeOperationId != 0L && activeOperationId != operationId) { + stopSelfResult(startId) + return START_NOT_STICKY + } + + activeOperationId = operationId + ensureNotificationChannel() + startForeground( + NOTIFICATION_ID, + buildNotification( + InstallServiceController.state.value.statusMessage.ifBlank { + getString(R.string.install_progress_title) + } + ) + ) + acquireWakeLock() + NativeLib.prepareFrontend() + + serviceScope.launch { + val result = runOperation(operationId, operationType, intent) + InstallServiceController.finish(operationId, result) + updateNotification(result.message) + releaseWakeLock() + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelfResult(startId) + activeOperationId = 0L + } + + return START_NOT_STICKY + } + + override fun onDestroy() { + releaseWakeLock() + serviceScope.cancel() + super.onDestroy() + } + + private suspend fun runOperation( + operationId: Long, + operationType: InstallOperationType, + intent: Intent + ): InstallResult = try { + when (operationType) { + InstallOperationType.FIRMWARE -> installFirmware( + operationId = operationId, + path = intent.getStringExtra(InstallServiceController.EXTRA_PATH).orEmpty() + ) + InstallOperationType.PKG -> installPkg( + operationId = operationId, + path = intent.getStringExtra(InstallServiceController.EXTRA_PATH).orEmpty(), + zrif = intent.getStringExtra(InstallServiceController.EXTRA_ZRIF).orEmpty(), + licensePath = intent.getStringExtra(InstallServiceController.EXTRA_LICENSE_PATH) + ) + InstallOperationType.ARCHIVE -> installArchive( + operationId = operationId, + path = intent.getStringExtra(InstallServiceController.EXTRA_PATH).orEmpty() + ) + InstallOperationType.ARCHIVE_FOLDER -> installArchiveFolder( + operationId = operationId, + paths = intent.getStringArrayListExtra(InstallServiceController.EXTRA_PATHS).orEmpty() + ) + InstallOperationType.LICENSE_FILE -> installLicense( + path = intent.getStringExtra(InstallServiceController.EXTRA_PATH).orEmpty() + ) + InstallOperationType.LICENSE_ZRIF -> installLicenseFromZrif( + zrif = intent.getStringExtra(InstallServiceController.EXTRA_ZRIF).orEmpty() + ) + } + } catch (e: Exception) { + InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_error_generic, e.message ?: "") + ) + } + + private suspend fun installFirmware(operationId: Long, path: String): InstallResult { + val deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_firmware_file, path) + ) + val version = InstallRepository.installFirmware(path) { pct, status -> + onProgress(operationId, pct, status) + } + return if (version.isNotEmpty()) { + InstallResult( + status = InstallResultStatus.SUCCESS, + message = getString(R.string.install_success_firmware, version), + deleteOptions = deleteOptions + ) + } else { + InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_failed_firmware), + deleteOptions = deleteOptions + ) + } + } + + private suspend fun installPkg( + operationId: Long, + path: String, + zrif: String, + licensePath: String? + ): InstallResult { + val deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_package_file, path), + deleteOption(R.string.install_delete_license_file, licensePath) + ) + val success = InstallRepository.installPkg(path, zrif) { pct, status -> + onProgress(operationId, pct, status) + } + return if (success) { + InstallResult( + status = InstallResultStatus.SUCCESS, + message = getString(R.string.install_success_package), + deleteOptions = deleteOptions + ) + } else { + InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_failed_package), + deleteOptions = deleteOptions + ) + } + } + + private suspend fun installArchive(operationId: Long, path: String): InstallResult { + val deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_archive_files, path) + ) + val success = InstallRepository.installArchive(path, forceReinstall = true) { pct, status -> + onProgress(operationId, pct, status) + } + return if (success) { + InstallResult( + status = InstallResultStatus.SUCCESS, + message = getString(R.string.install_success_archive), + deleteOptions = deleteOptions + ) + } else { + InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_failed_archive), + deleteOptions = deleteOptions + ) + } + } + + private suspend fun installArchiveFolder(operationId: Long, paths: List): InstallResult { + if (paths.isEmpty()) { + return InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_error_archive_folder_empty) + ) + } + + val deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_archive_files, paths) + ) + val total = paths.size + var successCount = 0 + + paths.forEachIndexed { index, path -> + val archiveName = File(path).name.ifBlank { path } + val batchStatus = getString( + R.string.install_status_archive_batch, + index + 1, + total, + archiveName + ) + InstallServiceController.updateProgress(operationId, ((index * 100f) / total).toInt(), batchStatus) + val success = InstallRepository.installArchive(path, forceReinstall = true) { pct, _ -> + val overall = ((index + (pct / 100f)) / total.toFloat()) * 100f + onProgress(operationId, overall.toInt(), batchStatus) + } + if (success) { + successCount++ + } + } + + val failureCount = total - successCount + return when { + successCount == total -> InstallResult( + status = InstallResultStatus.SUCCESS, + message = getString(R.string.install_success_archive_batch, successCount), + deleteOptions = deleteOptions + ) + successCount > 0 -> InstallResult( + status = InstallResultStatus.PARTIAL, + message = getString(R.string.install_partial_archive_batch, successCount, failureCount), + deleteOptions = deleteOptions + ) + else -> InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_failed_archive_batch, total), + deleteOptions = deleteOptions + ) + } + } + + private suspend fun installLicense(path: String): InstallResult { + val deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_license_file, path) + ) + val success = InstallRepository.copyLicense(path) + return if (success) { + InstallResult( + status = InstallResultStatus.SUCCESS, + message = getString(R.string.install_success_license), + deleteOptions = deleteOptions + ) + } else { + InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_failed_license), + deleteOptions = deleteOptions + ) + } + } + + private suspend fun installLicenseFromZrif(zrif: String): InstallResult { + val success = InstallRepository.createLicense(zrif) + return if (success) { + InstallResult( + status = InstallResultStatus.SUCCESS, + message = getString(R.string.install_success_license) + ) + } else { + InstallResult( + status = InstallResultStatus.ERROR, + message = getString(R.string.install_failed_license_zrif) + ) + } + } + + private fun onProgress(operationId: Long, percent: Int, status: String) { + InstallServiceController.updateProgress(operationId, percent, status) + updateNotification(status) + } + + private fun buildNotification(status: String) = + NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(getString(R.string.install_progress_title)) + .setContentText(status) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setContentIntent( + PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + + private fun updateNotification(status: String) { + ensureNotificationChannel() + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + manager.notify(NOTIFICATION_ID, buildNotification(status)) + } + + private fun ensureNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return + } + + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + getString(R.string.install_notification_channel_name), + NotificationManager.IMPORTANCE_LOW + ).apply { + description = getString(R.string.install_notification_channel_desc) + } + manager.createNotificationChannel(channel) + } + + private fun acquireWakeLock() { + if (wakeLock?.isHeld == true) { + return + } + + val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "org.vita3k.emulator:install" + ).apply { + setReferenceCounted(false) + acquire(WAKE_LOCK_TIMEOUT_MS) + } + } + + private fun releaseWakeLock() { + wakeLock?.let { lock -> + if (lock.isHeld) { + lock.release() + } + } + wakeLock = null + } + + private fun deleteOption(labelResId: Int, path: String?): DeleteSourceOption? = + deleteOption(labelResId, listOfNotNull(path)) + + private fun deleteOption(labelResId: Int, paths: List): DeleteSourceOption? { + val normalized = paths + .filter(String::isNotBlank) + .distinct() + if (normalized.isEmpty()) { + return null + } + + return DeleteSourceOption( + label = getString(labelResId), + paths = normalized + ) + } + + private fun sourceDeleteOptions(vararg options: DeleteSourceOption?): List = + options.filterNotNull() + + companion object { + private const val NOTIFICATION_CHANNEL_ID = "install_progress" + private const val NOTIFICATION_ID = 0x317 + private const val WAKE_LOCK_TIMEOUT_MS = 6L * 60L * 60L * 1000L + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/InstallServiceController.kt b/android/app/src/main/java/org/vita3k/emulator/InstallServiceController.kt new file mode 100644 index 000000000..73e31829e --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/InstallServiceController.kt @@ -0,0 +1,132 @@ +package org.vita3k.emulator + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.vita3k.emulator.ui.viewmodel.InstallResult +import java.util.concurrent.atomic.AtomicLong + +enum class InstallOperationType { + FIRMWARE, + PKG, + ARCHIVE, + ARCHIVE_FOLDER, + LICENSE_FILE, + LICENSE_ZRIF +} + +data class InstallServiceState( + val operationId: Long = 0L, + val operationType: InstallOperationType? = null, + val installing: Boolean = false, + val progress: Int = 0, + val statusMessage: String = "", + val installResult: InstallResult? = null +) + +object InstallServiceController { + const val EXTRA_OPERATION_ID = "operation_id" + const val EXTRA_OPERATION_TYPE = "operation_type" + const val EXTRA_PATH = "path" + const val EXTRA_PATHS = "paths" + const val EXTRA_ZRIF = "zrif" + const val EXTRA_LICENSE_PATH = "license_path" + + private val nextOperationId = AtomicLong(1L) + private val mutableState = MutableStateFlow(InstallServiceState()) + + val state: StateFlow = mutableState + + fun startFirmware(context: Context, path: String): Long = + start(context, InstallOperationType.FIRMWARE, context.getString(org.vita3k.emulator.R.string.install_status_firmware)) { + putExtra(EXTRA_PATH, path) + } + + fun startPkg(context: Context, path: String, zrif: String, licensePath: String?): Long = + start(context, InstallOperationType.PKG, context.getString(org.vita3k.emulator.R.string.install_status_package)) { + putExtra(EXTRA_PATH, path) + putExtra(EXTRA_ZRIF, zrif) + putExtra(EXTRA_LICENSE_PATH, licensePath) + } + + fun startArchive(context: Context, path: String): Long = + start(context, InstallOperationType.ARCHIVE, context.getString(org.vita3k.emulator.R.string.install_status_archive)) { + putExtra(EXTRA_PATH, path) + } + + fun startArchiveFolder(context: Context, paths: List): Long = + start(context, InstallOperationType.ARCHIVE_FOLDER, context.getString(org.vita3k.emulator.R.string.install_status_archive)) { + putStringArrayListExtra(EXTRA_PATHS, ArrayList(paths)) + } + + fun startLicenseFile(context: Context, path: String): Long = + start(context, InstallOperationType.LICENSE_FILE, context.getString(org.vita3k.emulator.R.string.install_status_license)) { + putExtra(EXTRA_PATH, path) + } + + fun startLicenseZrif(context: Context, zrif: String): Long = + start(context, InstallOperationType.LICENSE_ZRIF, context.getString(org.vita3k.emulator.R.string.install_status_license)) { + putExtra(EXTRA_ZRIF, zrif) + } + + fun updateProgress(operationId: Long, percent: Int, status: String) { + mutableState.update { state -> + if (state.operationId != operationId) { + state + } else { + state.copy( + installing = true, + progress = percent.coerceIn(0, 100), + statusMessage = status + ) + } + } + } + + fun finish(operationId: Long, result: InstallResult, progress: Int = 100, status: String = "") { + mutableState.update { state -> + if (state.operationId != operationId) { + state + } else { + state.copy( + installing = false, + progress = progress.coerceIn(0, 100), + statusMessage = status.ifBlank { state.statusMessage }, + installResult = result + ) + } + } + } + + fun clearResult() { + mutableState.update { state -> + state.copy(installResult = null) + } + } + + private inline fun start( + context: Context, + type: InstallOperationType, + initialStatus: String, + extras: Intent.() -> Unit + ): Long { + val operationId = nextOperationId.getAndIncrement() + mutableState.value = InstallServiceState( + operationId = operationId, + operationType = type, + installing = true, + statusMessage = initialStatus + ) + + val intent = Intent(context, InstallForegroundService::class.java).apply { + putExtra(EXTRA_OPERATION_ID, operationId) + putExtra(EXTRA_OPERATION_TYPE, type.name) + extras() + } + ContextCompat.startForegroundService(context, intent) + return operationId + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/MainActivity.kt b/android/app/src/main/java/org/vita3k/emulator/MainActivity.kt new file mode 100644 index 000000000..6eef2f579 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/MainActivity.kt @@ -0,0 +1,248 @@ +package org.vita3k.emulator + +import android.os.Build +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import org.vita3k.emulator.data.AppStorage +import org.vita3k.emulator.ui.navigation.AppNavigation +import org.vita3k.emulator.ui.theme.Vita3KTheme +import org.vita3k.emulator.ui.viewmodel.AppsListViewModel +import org.vita3k.emulator.ui.viewmodel.InstallViewModel +import org.vita3k.emulator.ui.viewmodel.SettingsViewModel +import org.vita3k.emulator.ui.viewmodel.UserManagementViewModel + +class MainActivity : AppCompatActivity() { + private val appsListViewModel: AppsListViewModel by viewModels() + private val installViewModel: InstallViewModel by viewModels() + private val settingsViewModel: SettingsViewModel by viewModels() + private val userManagementViewModel: UserManagementViewModel by viewModels() + + private val emulatorLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (appsListViewModel.initialized) { + appsListViewModel.reloadAppsList() + } + } + + private var pendingFolderCallback: ((String?) -> Unit)? = null + private var pendingFileCallback: ((String?) -> Unit)? = null + private var pendingArchiveFolderCallback: ((List) -> Unit)? = null + private var pendingInstallFileExtensions: Set? = null + private var pendingArchiveFolderExtensions: Set? = null + private var pendingStorageAction: (() -> Unit)? = null + + private val folderPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { + if (StorageAccess.hasStorageAccess(this)) { + launchPendingStorageAction() + } else { + cancelPendingStorageRequest() + } + } + + private val manageFolderAccessLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (StorageAccess.hasStorageAccess(this)) { + launchPendingStorageAction() + } else { + cancelPendingStorageRequest() + } + } + + private val filePickerLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val selectedPath = if (result.resultCode == RESULT_OK) { + result.data?.data?.let { uri -> StorageAccess.resolveUriToPath(this, uri) } + } else { + null + } + val validatedPath = selectedPath?.takeIf { path -> + pendingInstallFileExtensions?.let { allowed -> + StorageAccess.matchesAllowedExtension(path, allowed) + } ?: true + } + dispatchFileResult(validatedPath) + } + + private val folderPickerLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (pendingArchiveFolderCallback != null) { + val selectedPaths = if (result.resultCode == RESULT_OK) { + result.data?.data?.let { treeUri -> + StorageAccess.resolveTreeFilePaths( + context = this, + treeUri = treeUri, + allowedExtensions = pendingArchiveFolderExtensions ?: emptySet() + ) + } ?: emptyList() + } else { + emptyList() + } + dispatchArchiveFolderResult(selectedPaths) + return@registerForActivityResult + } + + val selectedPath = if (result.resultCode == RESULT_OK) { + result.data?.data?.let { uri -> StorageAccess.resolveTreeUriToPath(this, uri) } + } else { + null + } + dispatchFolderResult(selectedPath) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + prepareFrontendRuntime() + setTheme(R.style.Theme_Vita3K) + + val storagePath = AppStorage.storageRootPath(this) + appsListViewModel.initialize(storagePath) + + setContent { + Vita3KTheme { + AppNavigation( + appsListViewModel = appsListViewModel, + installViewModel = installViewModel, + settingsViewModel = settingsViewModel, + userManagementViewModel = userManagementViewModel, + onAppLaunch = { app -> launchApp(app.titleId, app.title) } + ) + } + } + } + + fun requestStorageFolderChange(onResult: (String?) -> Unit) { + requestFolderPath(onResult) + } + + fun requestFolderPath(onResult: (String?) -> Unit) { + pendingFolderCallback = onResult + pendingFileCallback = null + pendingArchiveFolderCallback = null + pendingInstallFileExtensions = null + pendingArchiveFolderExtensions = null + pendingStorageAction = { launchFolderPicker() } + ensureStorageAccess() + } + + fun requestFilePath(mimeTypes: Array, onResult: (String?) -> Unit) { + pendingFileCallback = onResult + pendingFolderCallback = null + pendingArchiveFolderCallback = null + pendingInstallFileExtensions = null + pendingArchiveFolderExtensions = null + pendingStorageAction = { launchFilePicker(mimeTypes) } + ensureStorageAccess() + } + + fun requestInstallFilePath( + allowedExtensions: Set, + onResult: (String?) -> Unit + ) { + pendingFileCallback = onResult + pendingFolderCallback = null + pendingArchiveFolderCallback = null + pendingInstallFileExtensions = allowedExtensions + pendingArchiveFolderExtensions = null + pendingStorageAction = { launchInstallFilePicker() } + ensureStorageAccess() + } + + fun requestArchiveFolderPaths( + allowedExtensions: Set, + onResult: (List) -> Unit + ) { + pendingFileCallback = null + pendingFolderCallback = null + pendingArchiveFolderCallback = onResult + pendingInstallFileExtensions = null + pendingArchiveFolderExtensions = allowedExtensions + pendingStorageAction = { launchInstallFolderPicker() } + ensureStorageAccess() + } + + override fun onResume() { + super.onResume() + prepareFrontendRuntime() + } + + private fun ensureStorageAccess() { + if (StorageAccess.hasStorageAccess(this)) { + launchPendingStorageAction() + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + manageFolderAccessLauncher.launch(StorageAccess.createManageAllFilesIntent(this)) + return + } + + folderPermissionLauncher.launch(StorageAccess.missingStoragePermissions(this)) + } + + private fun launchApp(titleId: String, appTitle: String) { + emulatorLauncher.launch(Emulator.createLaunchIntent(this, titleId, appTitle)) + } + + private fun launchFilePicker(mimeTypes: Array) { + filePickerLauncher.launch(StorageAccess.createFilePickerIntent(mimeTypes)) + } + + private fun launchInstallFilePicker() { + filePickerLauncher.launch(StorageAccess.createInstallFilePickerIntent()) + } + + private fun launchFolderPicker() { + folderPickerLauncher.launch(StorageAccess.createFolderPickerIntent()) + } + + private fun launchInstallFolderPicker() { + folderPickerLauncher.launch(StorageAccess.createInstallFolderPickerIntent()) + } + + private fun launchPendingStorageAction() { + val action = pendingStorageAction ?: return + pendingStorageAction = null + action.invoke() + } + + private fun cancelPendingStorageRequest() { + pendingStorageAction = null + dispatchFileResult(null) + dispatchFolderResult(null) + dispatchArchiveFolderResult(emptyList()) + } + + private fun dispatchFolderResult(path: String?) { + val callback = pendingFolderCallback + pendingFolderCallback = null + callback?.invoke(path?.takeIf { it.isNotBlank() }) + } + + private fun dispatchFileResult(path: String?) { + val callback = pendingFileCallback + pendingFileCallback = null + pendingInstallFileExtensions = null + callback?.invoke(path?.takeIf { it.isNotBlank() }) + } + + private fun dispatchArchiveFolderResult(paths: List) { + val callback = pendingArchiveFolderCallback + pendingArchiveFolderCallback = null + pendingArchiveFolderExtensions = null + callback?.invoke(paths) + } + + private fun prepareFrontendRuntime() { + NativeLib.prepareFrontend() + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/NativeLib.kt b/android/app/src/main/java/org/vita3k/emulator/NativeLib.kt new file mode 100644 index 000000000..de26d3167 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/NativeLib.kt @@ -0,0 +1,153 @@ +package org.vita3k.emulator + +import org.vita3k.emulator.data.EmulatorConfig +import org.vita3k.emulator.data.InstallCallback +import org.vita3k.emulator.data.NativeAppInfo +import org.vita3k.emulator.data.NativeUser + +/** + * JNI bridge to the Vita3K C++ backend. + * All methods are implemented in the native Android JNI modules under vita3k/android/jni/. + * Game boot/stop is handled by the SDL-based Emulator activity. + */ +object NativeLib { + const val PAUSE_REASON_USER = 1 shl 0 + const val PAUSE_REASON_MENU = 1 shl 1 + const val PAUSE_REASON_BACKGROUND = 1 shl 2 + + // --- Initialization --- + external fun prepareFrontend(): Boolean + external fun init(storagePath: String): Boolean + external fun isInitialized(): Boolean + external fun isOfficialBuild(): Boolean + + // --- Apps list --- + /** Returns detailed app info objects. */ + external fun getAppListDetailed(): Array + external fun refreshAppsList() + + // --- App actions --- + /** + * Dispatches a single app action identified by its AppActionMask bit. + * See AppAction.maskBit for the values. + */ + external fun performAppAction(titleId: String, actionBit: Int): Boolean + /** Returns a bitmask of available actions for the given title (AppActionMask bits). */ + external fun getAppActionAvailabilityMask(titleId: String): Int + + // --- Firmware / Info --- + external fun getAppVersion(): String + external fun getFirmwareInstallStateMask(): Int + external fun getCompatibilityDatabaseVersion(): String + external fun installCompatibilityDatabase(zipData: ByteArray, version: String): Boolean + /** Returns the installed application size in bytes, or 0 on error. */ + external fun getAppInstallSize(titleId: String): Long + + // --- Installation --- + /** Installs firmware from PUP file. Returns version string on success, empty on failure. */ + external fun installFirmware(path: String, callback: InstallCallback): String + /** Installs a .pkg file. Pass zrif="" to skip license. */ + external fun installPkg(path: String, zrif: String, callback: InstallCallback): Boolean + /** + * Installs a .zip or .vpk archive. + * @param forceReinstall If true, silently reinstalls if already present (no prompt). + */ + external fun installArchive(path: String, callback: InstallCallback, forceReinstall: Boolean): Boolean + /** Copies a .rif or .bin license file. */ + external fun copyLicense(path: String): Boolean + /** Creates and installs a license from a zRIF key. */ + external fun createLicense(zrif: String): Boolean + /** Auto-finds a zRIF for a PKG from existing licenses. Returns empty string if not found. */ + external fun findPkgZrif(pkgPath: String): String + /** Converts a .bin/.rif license file to a zRIF string. Returns empty string on failure. */ + external fun convertRifToZrif(path: String): String + + // --- Settings / Config --- + /** Returns the current global EmulatorConfig (all CurrentConfig + global fields). */ + external fun getGlobalConfig(): EmulatorConfig + /** Returns the currently active emulated storage folder path. */ + external fun getCurrentEmulatorPath(): String + /** Updates the active emulated storage folder path without changing the app storage root. */ + external fun setCurrentEmulatorPath(path: String): Boolean + /** Returns a fresh EmulatorConfig populated with emulator default values. */ + external fun getDefaultConfig(): EmulatorConfig + /** Returns true when a per-app custom config file exists for [titleId]. */ + external fun hasCustomConfig(titleId: String): Boolean + /** Returns the saved custom config for [titleId], or null when none exists. */ + external fun getCustomConfig(titleId: String): EmulatorConfig? + /** + * Saves either the global config (when [titleId] is null) or a per-app custom config. + * Returns the ids of the changed settings that still need a game restart when this save affected the active session. + */ + external fun saveSettings(titleId: String?, config: EmulatorConfig): IntArray + /** + * Deletes the per-app custom config file for [titleId]. + * Returns the ids of the changed settings that still need a game restart when this reset affected the active session. + */ + external fun deleteCustomConfig(titleId: String): IntArray + /** Deletes all per-app custom config files and returns how many were removed. */ + external fun clearAllCustomConfigs(): Int + /** Returns the support bitmask for Vulkan memory-mapping methods for the selected Android GPU driver. */ + external fun getSupportedMemoryMappingMask(customDriverName: String): Int + /** + * Returns [supportedMask, status] for the selected Android GPU driver. + * status: 0 = default driver, 1 = custom driver loaded, 2 = custom driver fell back to default. + */ + external fun getCustomDriverSupportInfo(customDriverName: String): IntArray + /** Returns true when the Android custom GPU driver option should be shown on this device. */ + external fun shouldShowCustomDriverOptions(): Boolean + /** Returns the names of the camera devices currently visible to SDL. */ + external fun getAvailableCameras(): Array + /** Returns a flat array [label0, mask0, label1, mask1, ...] of available ad-hoc addresses. */ + external fun getAvailableAdhocAddresses(): Array + /** Returns the names of all installed custom GPU drivers. */ + external fun getInstalledCustomDrivers(): Array + /** Installs a custom GPU driver from a ZIP archive and returns its installed name, or empty on failure. */ + external fun installCustomDriver(path: String): String + /** Removes an installed custom GPU driver by name. */ + external fun removeCustomDriver(driverName: String): Boolean + + // --- Users --- + /** Returns all frontend-visible Vita users with their active state. */ + external fun getUsers(): Array + /** Creates a user and returns its ID, or an empty string on failure. */ + external fun createUser(name: String): String + /** Activates the selected user and persists the choice. */ + external fun activateUser(userId: String): Boolean + /** Deletes the selected user and updates the active user when needed. */ + external fun deleteUser(userId: String): Boolean + + // --- Running app session --- + /** Blocks or releases gameplay input without changing the native pause state. */ + external fun setInputIntercepted(intercepted: Boolean): Boolean + /** Enables or clears a specific native pause reason bit for the running app session. */ + external fun setPauseReasonEnabled(reasonMask: Int, enabled: Boolean): Boolean + /** Returns true when an app session is currently active. */ + external fun isAppRunning(): Boolean + /** Requests a graceful shutdown of the currently running app session. */ + external fun requestAppQuit(): Boolean + /** Requests an orderly in-process relaunch into another app or executable. */ + external fun requestAppRelaunch( + titleId: String, + selfPath: String, + args: Array, + loadExecReason: Boolean + ): Boolean + /** Returns true when the currently running app session is paused. */ + external fun isAppPaused(): Boolean + /** Returns the title of the currently running app session, or an empty string when unavailable. */ + external fun getRunningAppTitle(): String + /** Returns true while either SceIme or CommonDialog IME is active. */ + external fun isImeActive(): Boolean + /** Submits the currently active IME session as if the user pressed Enter/OK. */ + external fun submitIme(): Boolean + /** Dismisses the currently active IME session as if the user pressed Close/Back. */ + external fun dismissIme(): Boolean + /** + * Returns a flat array [name0, "true"/"false", name1, "true"/"false", ...] listing all LLE + * modules available in the installed firmware. Each pair is (module name, enabled state), + * where "true" means the module is in [currentModules]. + */ + external fun getModulesList(currentModules: Array): Array + +} diff --git a/android/app/src/main/java/org/vita3k/emulator/StorageAccess.kt b/android/app/src/main/java/org/vita3k/emulator/StorageAccess.kt new file mode 100644 index 000000000..df0622aa7 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/StorageAccess.kt @@ -0,0 +1,138 @@ +package org.vita3k.emulator + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.core.content.ContextCompat +import androidx.documentfile.provider.DocumentFile + +object StorageAccess { + + @JvmStatic + fun hasStorageAccess(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Environment.isExternalStorageManager() + } else { + missingStoragePermissions(context).isEmpty() + } + } + + @JvmStatic + fun missingStoragePermissions(context: Context): Array { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return emptyArray() + } + + return arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ).filter { permission -> + ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED + }.toTypedArray() + } + + @JvmStatic + fun createManageAllFilesIntent(context: Context): Intent { + return Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + .setData(Uri.parse("package:${context.packageName}")) + } + + @JvmStatic + fun createFilePickerIntent(mimeTypes: Array): Intent { + val normalizedMimeTypes = mimeTypes + .map(String::trim) + .filter(String::isNotEmpty) + .distinct() + + val primaryMimeType = if (normalizedMimeTypes.size == 1) { + normalizedMimeTypes.first() + } else { + "*/*" + } + + return Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(primaryMimeType) + .putExtra(Intent.EXTRA_LOCAL_ONLY, true) + .apply { + if (normalizedMimeTypes.size > 1) { + putExtra(Intent.EXTRA_MIME_TYPES, normalizedMimeTypes.toTypedArray()) + } + } + } + + @JvmStatic + fun createInstallFilePickerIntent(): Intent { + return Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*") + } + + @JvmStatic + fun createFolderPickerIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .putExtra(Intent.EXTRA_LOCAL_ONLY, true) + } + + @JvmStatic + fun createInstallFolderPickerIntent(): Intent { + return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + } + + @JvmStatic + fun resolveUriToPath(context: Context, uri: Uri): String? { + return runCatching { + val descriptor = context.contentResolver.openFileDescriptor(uri, "r") ?: return null + val rawPath = descriptor.use { android.system.Os.readlink("/proc/self/fd/${it.fd}") } + normalizeResolvedPath(rawPath) + }.getOrNull() + } + + @JvmStatic + fun resolveTreeUriToPath(context: Context, treeUri: Uri): String? { + val resolvedTreeUri = DocumentFile.fromTreeUri(context, treeUri)?.uri ?: treeUri + return resolveUriToPath(context, resolvedTreeUri) + } + + @JvmStatic + fun resolveTreeFilePaths(context: Context, treeUri: Uri, allowedExtensions: Set): List { + val tree = DocumentFile.fromTreeUri(context, treeUri) ?: return emptyList() + val normalizedExtensions = allowedExtensions + .map(String::lowercase) + .toSet() + + return tree.listFiles() + .asSequence() + .filter(DocumentFile::isFile) + .filter { document -> matchesAllowedExtension(document.name, normalizedExtensions) } + .mapNotNull { document -> resolveUriToPath(context, document.uri) } + .filter(String::isNotBlank) + .distinct() + .sorted() + .toList() + } + + @JvmStatic + fun matchesAllowedExtension(pathOrName: String?, allowedExtensions: Set): Boolean { + if (allowedExtensions.isEmpty()) { + return true + } + + val lowerName = pathOrName?.lowercase() ?: return false + return allowedExtensions.any(lowerName::endsWith) + } + + private fun normalizeResolvedPath(rawPath: String): String { + if (!rawPath.startsWith("/mnt/user/")) { + return rawPath + } + + val relativePath = rawPath.removePrefix("/mnt/user/") + return "/storage" + relativePath.substring(relativePath.indexOf('/')) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/Vita3KApplication.kt b/android/app/src/main/java/org/vita3k/emulator/Vita3KApplication.kt new file mode 100644 index 000000000..8a70bef6e --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/Vita3KApplication.kt @@ -0,0 +1,14 @@ +package org.vita3k.emulator + +import android.app.Application +import org.libsdl.app.SDLActivity +import org.vita3k.emulator.data.UiLanguages + +class Vita3KApplication : Application() { + override fun onCreate() { + super.onCreate() + System.loadLibrary("Vita3K") + SDLActivity.nativeSetenv("SDL_ANDROID_ALLOW_RECREATE_ACTIVITY", "1") + UiLanguages.applyStored(this) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/VitaInputConnection.java b/android/app/src/main/java/org/vita3k/emulator/VitaInputConnection.java new file mode 100644 index 000000000..1b8ca982d --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/VitaInputConnection.java @@ -0,0 +1,71 @@ +package org.vita3k.emulator; + +import android.content.Context; +import android.view.KeyEvent; +import android.view.View; + +import org.libsdl.app.SDLInputConnection; + +public class VitaInputConnection extends SDLInputConnection { + public VitaInputConnection(View targetView, boolean fullEditor) { + super(targetView, fullEditor); + } + + private boolean isNativeImeActive() { + try { + return NativeLib.INSTANCE.isImeActive(); + } catch (Throwable ignored) { + return false; + } + } + + private Emulator getEmulator() { + Context context = mEditText.getContext(); + return context instanceof Emulator ? (Emulator) context : null; + } + + public void clearConnectionState() { + android.text.Editable content = getEditable(); + if (content != null) { + content.clear(); + removeComposingSpans(content); + } + mCommittedText = ""; + } + + private void submitFromKeyboard() { + Emulator emulator = getEmulator(); + if (emulator != null) { + emulator.completeImeFromKeyboard(mEditText); + } else { + try { + NativeLib.INSTANCE.submitIme(); + } catch (Throwable ignored) { + } + clearConnectionState(); + } + } + + @Override + public boolean performEditorAction(int actionCode) { + if (isNativeImeActive()) { + submitFromKeyboard(); + return true; + } + + return super.performEditorAction(actionCode); + } + + @Override + public boolean sendKeyEvent(KeyEvent event) { + if (isNativeImeActive() + && event.getAction() == KeyEvent.ACTION_UP + && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER + || event.getKeyCode() == KeyEvent.KEYCODE_NUMPAD_ENTER)) { + submitFromKeyboard(); + return true; + } + + return super.sendKeyEvent(event); + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/VitaTextEdit.java b/android/app/src/main/java/org/vita3k/emulator/VitaTextEdit.java new file mode 100644 index 000000000..1b9ecbbcf --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/VitaTextEdit.java @@ -0,0 +1,94 @@ +package org.vita3k.emulator; + +import android.content.Context; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +import org.libsdl.app.SDLDummyEdit; + +import java.lang.reflect.Field; + +public class VitaTextEdit extends SDLDummyEdit { + private static final Field IC_FIELD = resolveField("ic"); + private static final Field INPUT_TYPE_FIELD = resolveField("input_type"); + + public VitaTextEdit(Context context) { + super(context); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + InputConnection connection = new VitaInputConnection(this, true); + setFieldValue(IC_FIELD, this, connection); + + outAttrs.inputType = getInputTypeValue(this); + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | + EditorInfo.IME_FLAG_NO_FULLSCREEN; + + return connection; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { + try { + if (NativeLib.INSTANCE.isImeActive()) { + Context context = getContext(); + if (context instanceof Emulator) { + ((Emulator) context).dismissImeFromKeyboard(this); + return true; + } + } + } catch (Throwable ignored) { + } + } + + return super.onKeyPreIme(keyCode, event); + } + + public void clearConnectionState() { + Object connection = getFieldValue(IC_FIELD, this); + if (connection instanceof VitaInputConnection) { + ((VitaInputConnection) connection).clearConnectionState(); + } + } + + public static int getInputTypeValue(SDLDummyEdit edit) { + Object value = getFieldValue(INPUT_TYPE_FIELD, edit); + return value instanceof Integer ? (Integer) value : 0; + } + + private static Field resolveField(String name) { + try { + Field field = SDLDummyEdit.class.getDeclaredField(name); + field.setAccessible(true); + return field; + } catch (Exception e) { + return null; + } + } + + private static Object getFieldValue(Field field, Object target) { + if (field == null || target == null) { + return null; + } + + try { + return field.get(target); + } catch (Exception e) { + return null; + } + } + + private static void setFieldValue(Field field, Object target, Object value) { + if (field == null || target == null) { + return; + } + + try { + field.set(target, value); + } catch (Exception ignored) { + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/AppInfo.kt b/android/app/src/main/java/org/vita3k/emulator/data/AppInfo.kt new file mode 100644 index 000000000..64c22fa80 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/AppInfo.kt @@ -0,0 +1,49 @@ +package org.vita3k.emulator.data + +import androidx.annotation.StringRes +import java.io.File +import org.vita3k.emulator.R + +enum class CompatibilityState( + val value: Int, + val colorHex: Long, + val onColorHex: Long, + @StringRes val labelResId: Int +) { + UNKNOWN(-1, 0xFF8A8A8A, 0xFFFFFFFF, R.string.compat_unknown), + NOTHING(0, 0xFFFF0000, 0xFFFFFFFF, R.string.compat_nothing), + BOOTABLE(1, 0xFF621FA5, 0xFFFFFFFF, R.string.compat_bootable), + INTRO(2, 0xFFC71585, 0xFFFFFFFF, R.string.compat_intro), + MENU(3, 0xFF1D76DB, 0xFFFFFFFF, R.string.compat_menu), + INGAME_LESS(4, 0xFFE08A1E, 0xFFFFFFFF, R.string.compat_ingame_less), + INGAME_MORE(5, 0xFFFFD700, 0xFF000000, R.string.compat_ingame_more), + PLAYABLE(6, 0xFF0E8A16, 0xFFFFFFFF, R.string.compat_playable); + + companion object { + fun fromValue(value: Int): CompatibilityState = + entries.find { it.value == value } ?: UNKNOWN + } +} + +data class AppInfo( + val titleId: String, + val title: String, + val category: String = "", + val appVer: String = "", + val iconPath: String? = null, + val hasCustomConfig: Boolean = false, + val compatibility: CompatibilityState = CompatibilityState.UNKNOWN, + val lastPlayed: Long = 0, + val playtime: Long = 0 +) { + val iconFile: File? + get() = iconPath?.takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.exists() } +} + +enum class SortOption { + TITLE, LAST_PLAYED, COMPATIBILITY +} + +enum class ViewMode { + GRID, LIST +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/AppRepository.kt b/android/app/src/main/java/org/vita3k/emulator/data/AppRepository.kt new file mode 100644 index 000000000..3adfc715a --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/AppRepository.kt @@ -0,0 +1,213 @@ +package org.vita3k.emulator.data + +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.vita3k.emulator.NativeLib +import org.vita3k.emulator.ui.viewmodel.AppAction +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URL + +internal object AppRepository { + private const val TAG = "Vita3K" + private const val COMPAT_VERSION_URL = + "https://api.github.com/repos/Vita3K/compatibility/releases/latest" + private const val COMPAT_DB_URL = + "https://github.com/Vita3K/compatibility/releases/download/compat_db/app_compat_db.xml.zip" + private const val UPDATE_RELEASE_URL = + "https://api.github.com/repos/Vita3K/Vita3K/releases/tags/continuous" + private const val UPDATE_PAGE_URL = + "https://github.com/Vita3K/Vita3K/releases/tag/continuous" + private val compatVersionRegex = + Regex("""Last updated: (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}Z)""") + private val updateBuildRegex = Regex("""Vita3K Build:\s*(\d+)""") + private val currentBuildRegex = Regex("""^[^-]+-(\d+)(?:-|$)""") + private val updateMetadataLineRegex = + Regex("""^\s*(Corresponding commit:|Vita3K Build:)""", RegexOption.IGNORE_CASE) + + suspend fun initialize(storagePath: String): Boolean = withContext(Dispatchers.IO) { + NativeLib.init(storagePath) + } + + suspend fun getAppVersion(): String = withContext(Dispatchers.IO) { + NativeLib.getAppVersion() + } + + suspend fun getFirmwareInstallState(): FirmwareInstallState = withContext(Dispatchers.IO) { + FirmwareInstallState.fromMask(NativeLib.getFirmwareInstallStateMask()) + } + + suspend fun syncCompatibilityDatabase(): Boolean = withContext(Dispatchers.IO) { + try { + val currentVersion = NativeLib.getCompatibilityDatabaseVersion() + val versionResponse = httpGetString(COMPAT_VERSION_URL) ?: return@withContext false + val latestVersion = compatVersionRegex.find(versionResponse)?.groupValues?.getOrNull(1) + ?: return@withContext false + + if (latestVersion == currentVersion) { + return@withContext false + } + + val zipData = httpGetBytes(COMPAT_DB_URL) ?: return@withContext false + NativeLib.installCompatibilityDatabase(zipData, latestVersion) + } catch (error: Exception) { + Log.w(TAG, "Failed to sync compatibility database", error) + false + } + } + + suspend fun getAppList(): List = withContext(Dispatchers.IO) { + NativeLib.getAppListDetailed().map(::toAppInfo) + } + + suspend fun refreshAppsList() = withContext(Dispatchers.IO) { + NativeLib.refreshAppsList() + } + + suspend fun runAppAction(titleId: String, action: AppAction): Boolean = + withContext(Dispatchers.IO) { + NativeLib.performAppAction(titleId, action.maskBit) + } + + suspend fun getAvailableAppActions(titleId: String): Set = + withContext(Dispatchers.IO) { + AppAction.fromMask(NativeLib.getAppActionAvailabilityMask(titleId)) + } + + suspend fun getAppInstallSize(titleId: String): Long = withContext(Dispatchers.IO) { + NativeLib.getAppInstallSize(titleId) + } + + suspend fun checkForUpdates( + appVersion: String, + officialBuild: Boolean + ): UpdateCheckResult = withContext(Dispatchers.IO) { + val currentDisplayVersion = currentDisplayVersion(appVersion) + val response = httpGetString(UPDATE_RELEASE_URL) + ?: return@withContext UpdateCheckResult( + status = UpdateCheckStatus.Failed, + message = "Could not retrieve the latest Vita3K release information. Please try again later.", + currentDisplayVersion = currentDisplayVersion + ) + + val root = try { + JSONObject(response) + } catch (_: Exception) { + return@withContext UpdateCheckResult( + status = UpdateCheckStatus.Failed, + message = "The GitHub release response could not be parsed.", + currentDisplayVersion = currentDisplayVersion + ) + } + + val body = root.optString("body").orEmpty() + val latestBuildNumber = updateBuildRegex.find(body)?.groupValues?.getOrNull(1)?.toLongOrNull() + ?: return@withContext UpdateCheckResult( + status = UpdateCheckStatus.Failed, + message = "The latest release does not advertise a usable Vita3K build number.", + currentDisplayVersion = currentDisplayVersion + ) + + val releaseName = root.optString("name").trim() + val tagName = root.optString("tag_name").trim() + val releaseUrl = root.optString("html_url").trim().ifBlank { UPDATE_PAGE_URL } + val info = UpdateInfo( + version = releaseName.ifBlank { tagName }, + buildNumber = latestBuildNumber, + releaseUrl = releaseUrl, + publishedAt = root.optString("published_at").trim(), + notes = normalizedUpdateNotes(body) + ) + + val currentBuildNumber = currentBuildRegex.find(appVersion)?.groupValues?.getOrNull(1)?.toLongOrNull() ?: 0L + val buildDelta = latestBuildNumber - currentBuildNumber + + when { + !officialBuild -> UpdateCheckResult( + status = UpdateCheckStatus.CustomBuildCanUpdate, + message = "", + info = info, + currentDisplayVersion = currentDisplayVersion + ) + buildDelta > 0L -> UpdateCheckResult( + status = UpdateCheckStatus.UpdateAvailable, + message = "", + info = info, + currentDisplayVersion = currentDisplayVersion + ) + buildDelta < 0L -> UpdateCheckResult( + status = UpdateCheckStatus.CurrentBuildNewerThanLatest, + message = "This installation appears newer than the latest official continuous build.", + info = info, + currentDisplayVersion = currentDisplayVersion + ) + else -> UpdateCheckResult( + status = UpdateCheckStatus.UpToDate, + message = "You already have the latest official build installed.", + info = info, + currentDisplayVersion = currentDisplayVersion + ) + } + } + + private fun toAppInfo(native: NativeAppInfo): AppInfo = AppInfo( + titleId = native.titleId, + title = native.title, + category = native.category, + appVer = native.appVer, + iconPath = native.iconPath.ifEmpty { null }, + hasCustomConfig = native.hasCustomConfig, + compatibility = CompatibilityState.fromValue(native.compatibility), + lastPlayed = native.lastPlayed, + playtime = native.playtime + ) + + private fun currentDisplayVersion(appVersion: String): String { + if (appVersion.isBlank()) { + return "" + } + + val parts = appVersion.split("-", limit = 3) + val version = parts.getOrNull(0).orEmpty().ifBlank { appVersion } + val build = parts.getOrNull(1).orEmpty() + return if (build.isBlank()) version else "$version ($build)" + } + + private fun normalizedUpdateNotes(body: String): String { + val cleaned = body + .replace("\r\n", "\n") + .lineSequence() + .filterNot { line -> updateMetadataLineRegex.containsMatchIn(line.trim()) } + .dropWhile { it.isBlank() } + .toList() + .dropLastWhile { it.isBlank() } + + return cleaned.joinToString("\n").trim() + } + + private fun httpGetString(url: String): String? = + httpGetBytes(url)?.toString(Charsets.UTF_8) + + private fun httpGetBytes(url: String): ByteArray? { + val connection = (URL(url).openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + instanceFollowRedirects = true + connectTimeout = 10000 + readTimeout = 15000 + setRequestProperty("User-Agent", "Vita3K-Android") + setRequestProperty("Accept", "application/vnd.github+json") + } + + return try { + connection.connect() + if (connection.responseCode !in 200..299) { + null + } else { + connection.inputStream.use { it.readBytes() } + } + } finally { + connection.disconnect() + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/AppStorage.kt b/android/app/src/main/java/org/vita3k/emulator/data/AppStorage.kt new file mode 100644 index 000000000..b81048572 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/AppStorage.kt @@ -0,0 +1,30 @@ +package org.vita3k.emulator.data + +import android.content.Context +import java.io.File + +internal object AppStorage { + private const val PREFS_NAME = "vita3k_app" + private const val KEY_INITIAL_SETUP_COMPLETED = "initial_setup_completed" + private const val DEFAULT_STORAGE_DIR_NAME = "vita" + + fun storageRootPath(context: Context): String { + return (context.getExternalFilesDir(null) ?: context.filesDir ?: File(context.cacheDir, "files")).absolutePath + } + + fun defaultStoragePath(context: Context): String { + val baseDir = File(storageRootPath(context)) + return File(baseDir, DEFAULT_STORAGE_DIR_NAME).absolutePath + } + + fun isInitialSetupCompleted(context: Context): Boolean = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getBoolean(KEY_INITIAL_SETUP_COMPLETED, false) + + fun setInitialSetupCompleted(context: Context, completed: Boolean) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(KEY_INITIAL_SETUP_COMPLETED, completed) + .apply() + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/EmulatorConfig.kt b/android/app/src/main/java/org/vita3k/emulator/data/EmulatorConfig.kt new file mode 100644 index 000000000..52b064e97 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/EmulatorConfig.kt @@ -0,0 +1,339 @@ +package org.vita3k.emulator.data + +/** + * Mirrors the JNI-backed emulator settings object. + * + * All properties are exposed as @JvmField vars so the native layer can access + * them directly after constructing this class with the no-arg constructor. + */ +class EmulatorConfig { + + // Core + @JvmField var modulesMode: Int = 0 + @JvmField var lleModules: Array = emptyArray() + @JvmField var cpuOpt: Boolean = true + + // GPU + @JvmField var backendRenderer: String = "Vulkan" + @JvmField var highAccuracy: Boolean = false + @JvmField var resolutionMultiplier: Float = 1.0f + @JvmField var disableSurfaceSync: Boolean = true + @JvmField var screenFilter: String = "Bilinear" + @JvmField var memoryMapping: String = "double-buffer" + @JvmField var vSync: Boolean = true + @JvmField var anisotropicFiltering: Int = 1 + @JvmField var asyncPipelineCompilation: Boolean = true + @JvmField var exportTextures: Boolean = false + @JvmField var importTextures: Boolean = false + @JvmField var exportAsPng: Boolean = true + @JvmField var fpsHack: Boolean = false + @JvmField var turboMode: Boolean = false + @JvmField var shaderCache: Boolean = true + @JvmField var spirvShader: Boolean = false + @JvmField var customDriverName: String = "" + + // Audio + @JvmField var audioBackend: String = "SDL" + @JvmField var audioVolume: Int = 100 + @JvmField var ngsEnable: Boolean = true + @JvmField var disableMotion: Boolean = false + @JvmField var controllerAnalogMultiplier: Float = 1.0f + @JvmField var controllerBinds: IntArray = defaultControllerBinds() + @JvmField var controllerAxisBinds: IntArray = defaultControllerAxisBinds() + + // Camera + @JvmField var frontCameraType: Int = 2 + @JvmField var frontCameraId: String = "" + @JvmField var frontCameraImage: String = "" + @JvmField var frontCameraColor: Long = 0L + @JvmField var backCameraType: Int = 2 + @JvmField var backCameraId: String = "" + @JvmField var backCameraImage: String = "" + @JvmField var backCameraColor: Long = 0L + + // System + @JvmField var pstvMode: Boolean = false + @JvmField var showMode: Boolean = false + @JvmField var demoMode: Boolean = false + @JvmField var sysButton: Int = 1 + @JvmField var sysLang: Int = 1 + @JvmField var sysDateFormat: Int = 2 + @JvmField var sysTimeFormat: Int = 0 + @JvmField var imeLangs: Long = 4L + @JvmField var userLang: String = "" + + // Network + @JvmField var psnSignedIn: Boolean = false + @JvmField var httpEnable: Boolean = true + @JvmField var httpTimeoutAttempts: Int = 50 + @JvmField var httpTimeoutSleepMs: Int = 100 + @JvmField var httpReadEndAttempts: Int = 10 + @JvmField var httpReadEndSleepMs: Int = 250 + @JvmField var adhocAddr: Int = 0 + + // Debug + @JvmField var logImports: Boolean = false + @JvmField var logExports: Boolean = false + @JvmField var logActiveShaders: Boolean = false + @JvmField var logUniforms: Boolean = false + @JvmField var colorSurfaceDebug: Boolean = false + @JvmField var dumpElfs: Boolean = false + @JvmField var validationLayer: Boolean = true + @JvmField var textureCache: Boolean = true + @JvmField var stretchDisplayArea: Boolean = false + @JvmField var fullscreenHdResPixelPerfect: Boolean = false + @JvmField var fileLoadingDelay: Int = 0 + + // Emulator + @JvmField var showLiveAreaScreen: Boolean = false + @JvmField var showCompileShaders: Boolean = true + @JvmField var checkForUpdates: Boolean = true + @JvmField var checkForUpdatesMode: Int = 1 + @JvmField var archiveLog: Boolean = false + @JvmField var logCompatWarn: Boolean = false + @JvmField var logLevel: Int = 0 + @JvmField var performanceOverlay: Boolean = false + @JvmField var performanceOverlayDetail: Int = 0 + @JvmField var performanceOverlayPosition: Int = 0 + @JvmField var screenshotFormat: Int = 1 + + fun copy(): EmulatorConfig = EmulatorConfig().also { config -> + config.modulesMode = modulesMode + config.lleModules = lleModules.copyOf() + config.cpuOpt = cpuOpt + config.backendRenderer = backendRenderer + config.highAccuracy = highAccuracy + config.resolutionMultiplier = resolutionMultiplier + config.disableSurfaceSync = disableSurfaceSync + config.screenFilter = screenFilter + config.memoryMapping = memoryMapping + config.vSync = vSync + config.anisotropicFiltering = anisotropicFiltering + config.asyncPipelineCompilation = asyncPipelineCompilation + config.exportTextures = exportTextures + config.importTextures = importTextures + config.exportAsPng = exportAsPng + config.fpsHack = fpsHack + config.turboMode = turboMode + config.shaderCache = shaderCache + config.spirvShader = spirvShader + config.customDriverName = customDriverName + config.audioBackend = audioBackend + config.audioVolume = audioVolume + config.ngsEnable = ngsEnable + config.disableMotion = disableMotion + config.controllerAnalogMultiplier = controllerAnalogMultiplier + config.controllerBinds = controllerBinds.copyOf() + config.controllerAxisBinds = controllerAxisBinds.copyOf() + config.frontCameraType = frontCameraType + config.frontCameraId = frontCameraId + config.frontCameraImage = frontCameraImage + config.frontCameraColor = frontCameraColor + config.backCameraType = backCameraType + config.backCameraId = backCameraId + config.backCameraImage = backCameraImage + config.backCameraColor = backCameraColor + config.pstvMode = pstvMode + config.showMode = showMode + config.demoMode = demoMode + config.sysButton = sysButton + config.sysLang = sysLang + config.sysDateFormat = sysDateFormat + config.sysTimeFormat = sysTimeFormat + config.imeLangs = imeLangs + config.userLang = userLang + config.psnSignedIn = psnSignedIn + config.httpEnable = httpEnable + config.httpTimeoutAttempts = httpTimeoutAttempts + config.httpTimeoutSleepMs = httpTimeoutSleepMs + config.httpReadEndAttempts = httpReadEndAttempts + config.httpReadEndSleepMs = httpReadEndSleepMs + config.adhocAddr = adhocAddr + config.logImports = logImports + config.logExports = logExports + config.logActiveShaders = logActiveShaders + config.logUniforms = logUniforms + config.colorSurfaceDebug = colorSurfaceDebug + config.dumpElfs = dumpElfs + config.validationLayer = validationLayer + config.textureCache = textureCache + config.stretchDisplayArea = stretchDisplayArea + config.fullscreenHdResPixelPerfect = fullscreenHdResPixelPerfect + config.fileLoadingDelay = fileLoadingDelay + config.showLiveAreaScreen = showLiveAreaScreen + config.showCompileShaders = showCompileShaders + config.checkForUpdates = checkForUpdates + config.checkForUpdatesMode = checkForUpdatesMode + config.archiveLog = archiveLog + config.logCompatWarn = logCompatWarn + config.logLevel = logLevel + config.performanceOverlay = performanceOverlay + config.performanceOverlayDetail = performanceOverlayDetail + config.performanceOverlayPosition = performanceOverlayPosition + config.screenshotFormat = screenshotFormat + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is EmulatorConfig) return false + + return modulesMode == other.modulesMode && + lleModules.contentEquals(other.lleModules) && + cpuOpt == other.cpuOpt && + backendRenderer == other.backendRenderer && + highAccuracy == other.highAccuracy && + resolutionMultiplier == other.resolutionMultiplier && + disableSurfaceSync == other.disableSurfaceSync && + screenFilter == other.screenFilter && + memoryMapping == other.memoryMapping && + vSync == other.vSync && + anisotropicFiltering == other.anisotropicFiltering && + asyncPipelineCompilation == other.asyncPipelineCompilation && + exportTextures == other.exportTextures && + importTextures == other.importTextures && + exportAsPng == other.exportAsPng && + fpsHack == other.fpsHack && + turboMode == other.turboMode && + shaderCache == other.shaderCache && + spirvShader == other.spirvShader && + customDriverName == other.customDriverName && + audioBackend == other.audioBackend && + audioVolume == other.audioVolume && + ngsEnable == other.ngsEnable && + disableMotion == other.disableMotion && + controllerAnalogMultiplier == other.controllerAnalogMultiplier && + controllerBinds.contentEquals(other.controllerBinds) && + controllerAxisBinds.contentEquals(other.controllerAxisBinds) && + frontCameraType == other.frontCameraType && + frontCameraId == other.frontCameraId && + frontCameraImage == other.frontCameraImage && + frontCameraColor == other.frontCameraColor && + backCameraType == other.backCameraType && + backCameraId == other.backCameraId && + backCameraImage == other.backCameraImage && + backCameraColor == other.backCameraColor && + pstvMode == other.pstvMode && + showMode == other.showMode && + demoMode == other.demoMode && + sysButton == other.sysButton && + sysLang == other.sysLang && + sysDateFormat == other.sysDateFormat && + sysTimeFormat == other.sysTimeFormat && + imeLangs == other.imeLangs && + userLang == other.userLang && + psnSignedIn == other.psnSignedIn && + httpEnable == other.httpEnable && + httpTimeoutAttempts == other.httpTimeoutAttempts && + httpTimeoutSleepMs == other.httpTimeoutSleepMs && + httpReadEndAttempts == other.httpReadEndAttempts && + httpReadEndSleepMs == other.httpReadEndSleepMs && + adhocAddr == other.adhocAddr && + logImports == other.logImports && + logExports == other.logExports && + logActiveShaders == other.logActiveShaders && + logUniforms == other.logUniforms && + colorSurfaceDebug == other.colorSurfaceDebug && + dumpElfs == other.dumpElfs && + validationLayer == other.validationLayer && + textureCache == other.textureCache && + stretchDisplayArea == other.stretchDisplayArea && + fullscreenHdResPixelPerfect == other.fullscreenHdResPixelPerfect && + fileLoadingDelay == other.fileLoadingDelay && + showLiveAreaScreen == other.showLiveAreaScreen && + showCompileShaders == other.showCompileShaders && + checkForUpdates == other.checkForUpdates && + checkForUpdatesMode == other.checkForUpdatesMode && + archiveLog == other.archiveLog && + logCompatWarn == other.logCompatWarn && + logLevel == other.logLevel && + performanceOverlay == other.performanceOverlay && + performanceOverlayDetail == other.performanceOverlayDetail && + performanceOverlayPosition == other.performanceOverlayPosition && + screenshotFormat == other.screenshotFormat + } + + override fun hashCode(): Int { + var result = modulesMode + result = 31 * result + lleModules.contentHashCode() + result = 31 * result + cpuOpt.hashCode() + result = 31 * result + backendRenderer.hashCode() + result = 31 * result + highAccuracy.hashCode() + result = 31 * result + resolutionMultiplier.hashCode() + result = 31 * result + disableSurfaceSync.hashCode() + result = 31 * result + screenFilter.hashCode() + result = 31 * result + memoryMapping.hashCode() + result = 31 * result + vSync.hashCode() + result = 31 * result + anisotropicFiltering + result = 31 * result + asyncPipelineCompilation.hashCode() + result = 31 * result + exportTextures.hashCode() + result = 31 * result + importTextures.hashCode() + result = 31 * result + exportAsPng.hashCode() + result = 31 * result + fpsHack.hashCode() + result = 31 * result + turboMode.hashCode() + result = 31 * result + shaderCache.hashCode() + result = 31 * result + spirvShader.hashCode() + result = 31 * result + customDriverName.hashCode() + result = 31 * result + audioBackend.hashCode() + result = 31 * result + audioVolume + result = 31 * result + ngsEnable.hashCode() + result = 31 * result + disableMotion.hashCode() + result = 31 * result + controllerAnalogMultiplier.hashCode() + result = 31 * result + controllerBinds.contentHashCode() + result = 31 * result + controllerAxisBinds.contentHashCode() + result = 31 * result + frontCameraType + result = 31 * result + frontCameraId.hashCode() + result = 31 * result + frontCameraImage.hashCode() + result = 31 * result + frontCameraColor.hashCode() + result = 31 * result + backCameraType + result = 31 * result + backCameraId.hashCode() + result = 31 * result + backCameraImage.hashCode() + result = 31 * result + backCameraColor.hashCode() + result = 31 * result + pstvMode.hashCode() + result = 31 * result + showMode.hashCode() + result = 31 * result + demoMode.hashCode() + result = 31 * result + sysButton + result = 31 * result + sysLang + result = 31 * result + sysDateFormat + result = 31 * result + sysTimeFormat + result = 31 * result + imeLangs.hashCode() + result = 31 * result + userLang.hashCode() + result = 31 * result + psnSignedIn.hashCode() + result = 31 * result + httpEnable.hashCode() + result = 31 * result + httpTimeoutAttempts + result = 31 * result + httpTimeoutSleepMs + result = 31 * result + httpReadEndAttempts + result = 31 * result + httpReadEndSleepMs + result = 31 * result + adhocAddr + result = 31 * result + logImports.hashCode() + result = 31 * result + logExports.hashCode() + result = 31 * result + logActiveShaders.hashCode() + result = 31 * result + logUniforms.hashCode() + result = 31 * result + colorSurfaceDebug.hashCode() + result = 31 * result + dumpElfs.hashCode() + result = 31 * result + validationLayer.hashCode() + result = 31 * result + textureCache.hashCode() + result = 31 * result + stretchDisplayArea.hashCode() + result = 31 * result + fullscreenHdResPixelPerfect.hashCode() + result = 31 * result + fileLoadingDelay + result = 31 * result + showLiveAreaScreen.hashCode() + result = 31 * result + showCompileShaders.hashCode() + result = 31 * result + checkForUpdates.hashCode() + result = 31 * result + checkForUpdatesMode + result = 31 * result + archiveLog.hashCode() + result = 31 * result + logCompatWarn.hashCode() + result = 31 * result + logLevel + result = 31 * result + performanceOverlay.hashCode() + result = 31 * result + performanceOverlayDetail + result = 31 * result + performanceOverlayPosition + result = 31 * result + screenshotFormat + return result + } +} + +private fun defaultControllerBinds(): IntArray = intArrayOf( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 +) + +private fun defaultControllerAxisBinds(): IntArray = intArrayOf( + 0, 1, 2, 3, 4, 5 +) diff --git a/android/app/src/main/java/org/vita3k/emulator/data/FirmwareInstallState.kt b/android/app/src/main/java/org/vita3k/emulator/data/FirmwareInstallState.kt new file mode 100644 index 000000000..a683b2d14 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/FirmwareInstallState.kt @@ -0,0 +1,91 @@ +package org.vita3k.emulator.data + +enum class FirmwareComponent { + Preinstalled, + Main, + FontPackage +} + +data class FirmwareComponentPresence( + val preinstalled: Boolean = false, + val main: Boolean = false, + val fontPackage: Boolean = false +) { + val hasAnyInstalled: Boolean + get() = preinstalled || main || fontPackage + + val missingComponents: Set + get() = buildSet { + if (!preinstalled) add(FirmwareComponent.Preinstalled) + if (!main) add(FirmwareComponent.Main) + if (!fontPackage) add(FirmwareComponent.FontPackage) + } + + val missingComponentsInOrder: List + get() = buildList { + if (!preinstalled) add(FirmwareComponent.Preinstalled) + if (!main) add(FirmwareComponent.Main) + if (!fontPackage) add(FirmwareComponent.FontPackage) + } + + val missingRequiredComponentsInOrder: List + get() = buildList { + if (!main) add(FirmwareComponent.Main) + if (!fontPackage) add(FirmwareComponent.FontPackage) + } +} + +sealed interface FirmwareInstallState { + val components: FirmwareComponentPresence + val hasRequiredComponents: Boolean + + val hasAnyInstalled: Boolean + get() = components.hasAnyInstalled + + val hasMainFirmware: Boolean + get() = components.main + + val hasFontPackage: Boolean + get() = components.fontPackage + + val isComplete: Boolean + get() = this is Complete + + data object Missing : FirmwareInstallState { + override val components = FirmwareComponentPresence() + override val hasRequiredComponents = false + } + + data class Partial( + override val components: FirmwareComponentPresence, + override val hasRequiredComponents: Boolean + ) : FirmwareInstallState + + data class Complete( + override val components: FirmwareComponentPresence + ) : FirmwareInstallState { + override val hasRequiredComponents = true + } + + companion object { + private const val PREINSTALLED_MASK = 1 shl 0 + private const val MAIN_MASK = 1 shl 1 + private const val FONT_MASK = 1 shl 2 + + fun fromMask(mask: Int): FirmwareInstallState { + val components = FirmwareComponentPresence( + preinstalled = mask and PREINSTALLED_MASK != 0, + main = mask and MAIN_MASK != 0, + fontPackage = mask and FONT_MASK != 0 + ) + + val hasRequiredComponents = components.main && components.fontPackage + + return when { + !components.hasAnyInstalled -> Missing + components.missingComponents.isEmpty() -> Complete(components) + else -> Partial(components, hasRequiredComponents) + } + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/FirmwareLinks.kt b/android/app/src/main/java/org/vita3k/emulator/data/FirmwareLinks.kt new file mode 100644 index 000000000..47df54357 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/FirmwareLinks.kt @@ -0,0 +1,45 @@ +package org.vita3k.emulator.data + +import androidx.annotation.StringRes +import org.vita3k.emulator.R + +data class FirmwareLocale( + @StringRes val nameResId: Int, + val code: String +) + +object FirmwareLinks { + const val PREINSTALL_URL = "https://bit.ly/4hlePsX" + const val FONT_PACKAGE_URL = "https://bit.ly/2P2rb0r" + + val locales = listOf( + FirmwareLocale(R.string.settings_lang_japanese, "ja-jp"), + FirmwareLocale(R.string.settings_lang_english_us, "en-us"), + FirmwareLocale(R.string.settings_lang_french, "fr-fr"), + FirmwareLocale(R.string.settings_lang_spanish, "es-es"), + FirmwareLocale(R.string.settings_lang_german, "de-de"), + FirmwareLocale(R.string.settings_lang_italian, "it-it"), + FirmwareLocale(R.string.settings_lang_dutch, "nl-nl"), + FirmwareLocale(R.string.settings_lang_portuguese_pt, "pt-pt"), + FirmwareLocale(R.string.settings_lang_russian, "ru-ru"), + FirmwareLocale(R.string.settings_lang_korean, "ko-kr"), + FirmwareLocale(R.string.settings_lang_chinese_trad, "zh-hant-hk"), + FirmwareLocale(R.string.settings_lang_chinese_simp, "zh-hans-cn"), + FirmwareLocale(R.string.settings_lang_finnish, "fi-fi"), + FirmwareLocale(R.string.settings_lang_swedish, "sv-se"), + FirmwareLocale(R.string.settings_lang_danish, "da-dk"), + FirmwareLocale(R.string.settings_lang_norwegian, "no-no"), + FirmwareLocale(R.string.settings_lang_polish, "pl-pl"), + FirmwareLocale(R.string.settings_lang_portuguese_br, "pt-br"), + FirmwareLocale(R.string.settings_lang_english_gb, "en-gb"), + FirmwareLocale(R.string.settings_lang_turkish, "tr-tr") + ) + + fun coerceLocaleIndex(index: Int): Int = + index.coerceIn(0, locales.lastIndex) + + fun firmwareDownloadUrl(index: Int): String { + val locale = locales[coerceLocaleIndex(index)] + return "https://www.playstation.com/${locale.code}/support/hardware/psvita/system-software/" + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/InstallCallback.kt b/android/app/src/main/java/org/vita3k/emulator/data/InstallCallback.kt new file mode 100644 index 000000000..9ae061a24 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/InstallCallback.kt @@ -0,0 +1,5 @@ +package org.vita3k.emulator.data + +interface InstallCallback { + fun onProgress(percent: Int, status: String) +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/InstallRepository.kt b/android/app/src/main/java/org/vita3k/emulator/data/InstallRepository.kt new file mode 100644 index 000000000..47ab2d176 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/InstallRepository.kt @@ -0,0 +1,45 @@ +package org.vita3k.emulator.data + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.vita3k.emulator.NativeLib + +internal object InstallRepository { + + suspend fun installFirmware(path: String, onProgress: (Int, String) -> Unit): String = + withContext(Dispatchers.IO) { + NativeLib.installFirmware(path, progressCallback(onProgress)) + } + + suspend fun installPkg(path: String, zrif: String = "", onProgress: (Int, String) -> Unit): Boolean = + withContext(Dispatchers.IO) { + NativeLib.installPkg(path, zrif, progressCallback(onProgress)) + } + + suspend fun installArchive(path: String, forceReinstall: Boolean, onProgress: (Int, String) -> Unit): Boolean = + withContext(Dispatchers.IO) { + NativeLib.installArchive(path, progressCallback(onProgress), forceReinstall) + } + + suspend fun copyLicense(path: String): Boolean = withContext(Dispatchers.IO) { + NativeLib.copyLicense(path) + } + + suspend fun createLicense(zrif: String): Boolean = withContext(Dispatchers.IO) { + NativeLib.createLicense(zrif) + } + + suspend fun findPkgZrif(pkgPath: String): String = withContext(Dispatchers.IO) { + NativeLib.findPkgZrif(pkgPath) + } + + suspend fun convertRifToZrif(path: String): String = withContext(Dispatchers.IO) { + NativeLib.convertRifToZrif(path) + } + + private fun progressCallback(onProgress: (Int, String) -> Unit) = object : InstallCallback { + override fun onProgress(percent: Int, status: String) { + onProgress(percent, status) + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/NativeAppInfo.kt b/android/app/src/main/java/org/vita3k/emulator/data/NativeAppInfo.kt new file mode 100644 index 000000000..16dce1bea --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/NativeAppInfo.kt @@ -0,0 +1,13 @@ +package org.vita3k.emulator.data + +data class NativeAppInfo( + val titleId: String, + val title: String, + val category: String, + val appVer: String, + val iconPath: String, + val hasCustomConfig: Boolean, + val compatibility: Int, + val lastPlayed: Long, + val playtime: Long +) diff --git a/android/app/src/main/java/org/vita3k/emulator/data/NativeImeState.kt b/android/app/src/main/java/org/vita3k/emulator/data/NativeImeState.kt new file mode 100644 index 000000000..a5ebb9066 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/NativeImeState.kt @@ -0,0 +1,12 @@ +package org.vita3k.emulator.data + +data class NativeImeState( + val sceImeActive: Boolean, + val dialogActive: Boolean, + val text: String, + val preeditStart: Int, + val preeditLength: Int, + val caretIndex: Int, + val multiline: Boolean, + val enterLabel: String +) diff --git a/android/app/src/main/java/org/vita3k/emulator/data/NativeUser.kt b/android/app/src/main/java/org/vita3k/emulator/data/NativeUser.kt new file mode 100644 index 000000000..f45ecab2f --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/NativeUser.kt @@ -0,0 +1,7 @@ +package org.vita3k.emulator.data + +data class NativeUser( + val id: String, + val name: String, + val active: Boolean +) diff --git a/android/app/src/main/java/org/vita3k/emulator/data/RestartRequiredSetting.kt b/android/app/src/main/java/org/vita3k/emulator/data/RestartRequiredSetting.kt new file mode 100644 index 000000000..a66b2f8e2 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/RestartRequiredSetting.kt @@ -0,0 +1,31 @@ +package org.vita3k.emulator.data + +import androidx.annotation.StringRes +import org.vita3k.emulator.R + +enum class RestartRequiredSetting(val nativeId: Int, @StringRes val labelResId: Int) { + CpuOpt(0, R.string.settings_cpu_opt), + BackendRenderer(1, R.string.settings_gpu_backend), + GraphicsDevice(2, R.string.settings_gpu_graphics_device), + CustomDriver(3, R.string.settings_gpu_custom_driver), + HighAccuracy(4, R.string.settings_gpu_accuracy), + ResolutionMultiplier(5, R.string.settings_gpu_resolution), + MemoryMapping(6, R.string.settings_gpu_memory_mapping), + AudioBackend(7, R.string.settings_audio_backend), + ValidationLayer(8, R.string.settings_debug_validation_layer); + + companion object { + fun fromNative(values: IntArray): List { + if (values.isEmpty()) { + return emptyList() + } + + val byId = entries.associateBy(RestartRequiredSetting::nativeId) + return buildList(values.size) { + values.forEach { nativeId -> + byId[nativeId]?.let(::add) + } + } + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/SettingsRepository.kt b/android/app/src/main/java/org/vita3k/emulator/data/SettingsRepository.kt new file mode 100644 index 000000000..5ba2cc9fc --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/SettingsRepository.kt @@ -0,0 +1,178 @@ +package org.vita3k.emulator.data + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.vita3k.emulator.NativeLib + +internal data class SettingsSnapshot( + val config: EmulatorConfig, + val emulatorStoragePath: String, + val hasCustomConfig: Boolean, + val modulesList: List>, + val installedCustomDrivers: List, + val showCustomDriverOptions: Boolean, + val supportedMemoryMappingMask: Int, + val customDriverLoadStatus: CustomDriverLoadStatus, + val availableCameras: List, + val availableAdhocAddresses: List> +) + +enum class CustomDriverLoadStatus { + Default, + Loaded, + Fallback; + + companion object { + fun fromNative(value: Int): CustomDriverLoadStatus = when (value) { + 1 -> Loaded + 2 -> Fallback + else -> Default + } + } +} + +data class CustomDriverSupportInfo( + val supportedMemoryMappingMask: Int, + val loadStatus: CustomDriverLoadStatus +) + +internal data class SettingsSaveResult( + val hasCustomConfig: Boolean, + val restartRequiredSettings: List +) + +internal data class DeleteCustomConfigResult( + val snapshot: SettingsSnapshot, + val restartRequiredSettings: List +) + +internal object SettingsRepository { + private val customDriverSupportInfoCache = mutableMapOf() + + + suspend fun load(titleId: String?): SettingsSnapshot = withContext(Dispatchers.IO) { + val hasCustomConfig = titleId != null && NativeLib.hasCustomConfig(titleId) + val loadedConfig = when { + titleId == null -> NativeLib.getGlobalConfig() + hasCustomConfig -> requireNotNull(NativeLib.getCustomConfig(titleId)) + else -> NativeLib.getGlobalConfig() + } + buildSnapshot(loadedConfig, hasCustomConfig) + } + + suspend fun save(routeTitleId: String?, effectiveTitleId: String?, config: EmulatorConfig): SettingsSaveResult { + val restartRequiredSettings = RestartRequiredSetting.fromNative(NativeLib.saveSettings(effectiveTitleId, config)) + return SettingsSaveResult( + hasCustomConfig = routeTitleId != null && NativeLib.hasCustomConfig(routeTitleId), + restartRequiredSettings = restartRequiredSettings + ) + } + + suspend fun loadDefaults(): SettingsSnapshot = withContext(Dispatchers.IO) { + buildSnapshot(NativeLib.getDefaultConfig(), hasCustomConfig = false) + } + + suspend fun setCurrentEmulatorPath(path: String): Boolean = withContext(Dispatchers.IO) { + NativeLib.setCurrentEmulatorPath(path) + } + + suspend fun deleteCustomConfig(titleId: String): DeleteCustomConfigResult { + val restartRequiredSettings = RestartRequiredSetting.fromNative(NativeLib.deleteCustomConfig(titleId)) + return DeleteCustomConfigResult( + snapshot = buildSnapshot(NativeLib.getGlobalConfig(), hasCustomConfig = false), + restartRequiredSettings = restartRequiredSettings + ) + } + + suspend fun clearAllCustomConfigs(): Int = withContext(Dispatchers.IO) { + NativeLib.clearAllCustomConfigs() + } + + suspend fun getInstalledCustomDrivers(): List = withContext(Dispatchers.IO) { + NativeLib.getInstalledCustomDrivers().toList() + } + + suspend fun installCustomDriver(path: String): String = withContext(Dispatchers.IO) { + NativeLib.installCustomDriver(path).also { installedName -> + if (installedName.isNotEmpty()) { + synchronized(customDriverSupportInfoCache) { + customDriverSupportInfoCache.remove(installedName) + } + } + } + } + + suspend fun removeCustomDriver(driverName: String): Boolean = withContext(Dispatchers.IO) { + NativeLib.removeCustomDriver(driverName).also { removed -> + if (removed) { + synchronized(customDriverSupportInfoCache) { + customDriverSupportInfoCache.remove(driverName) + } + } + } + } + + suspend fun getSupportedMemoryMappingMask(customDriverName: String): Int = withContext(Dispatchers.IO) { + NativeLib.getSupportedMemoryMappingMask(customDriverName) + } + + suspend fun getCustomDriverSupportInfo(customDriverName: String): CustomDriverSupportInfo = withContext(Dispatchers.IO) { + getCustomDriverSupportInfoInternal(customDriverName) + } + + private fun getCustomDriverSupportInfoInternal(customDriverName: String): CustomDriverSupportInfo { + if (customDriverName.isNotEmpty()) { + synchronized(customDriverSupportInfoCache) { + customDriverSupportInfoCache[customDriverName]?.let { return it } + } + } + + val raw = NativeLib.getCustomDriverSupportInfo(customDriverName) + val supportedMask = raw.getOrNull(0) ?: NativeLib.getSupportedMemoryMappingMask(customDriverName) + val loadStatus = CustomDriverLoadStatus.fromNative(raw.getOrNull(1) ?: 0) + val supportInfo = CustomDriverSupportInfo( + supportedMemoryMappingMask = supportedMask, + loadStatus = loadStatus + ) + + if (customDriverName.isNotEmpty()) { + synchronized(customDriverSupportInfoCache) { + customDriverSupportInfoCache.putIfAbsent(customDriverName, supportInfo) + } + } + + return supportInfo + } + + private fun buildSnapshot(config: EmulatorConfig, hasCustomConfig: Boolean): SettingsSnapshot { + val driverSupportInfo = getCustomDriverSupportInfoInternal(config.customDriverName) + return SettingsSnapshot( + config = config, + emulatorStoragePath = NativeLib.getCurrentEmulatorPath(), + hasCustomConfig = hasCustomConfig, + modulesList = parseModules(NativeLib.getModulesList(config.lleModules)), + installedCustomDrivers = NativeLib.getInstalledCustomDrivers().toList(), + showCustomDriverOptions = NativeLib.shouldShowCustomDriverOptions(), + supportedMemoryMappingMask = driverSupportInfo.supportedMemoryMappingMask, + customDriverLoadStatus = driverSupportInfo.loadStatus, + availableCameras = NativeLib.getAvailableCameras().toList(), + availableAdhocAddresses = parsePairs(NativeLib.getAvailableAdhocAddresses()) + ) + } + + private fun parseModules(rawModules: Array): List> = buildList { + var index = 0 + while (index + 1 < rawModules.size) { + add(rawModules[index] to (rawModules[index + 1] == "true")) + index += 2 + } + } + + private fun parsePairs(rawPairs: Array): List> = buildList { + var index = 0 + while (index + 1 < rawPairs.size) { + add(rawPairs[index] to rawPairs[index + 1]) + index += 2 + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/UiLanguages.kt b/android/app/src/main/java/org/vita3k/emulator/data/UiLanguages.kt new file mode 100644 index 000000000..f80832224 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/UiLanguages.kt @@ -0,0 +1,45 @@ +package org.vita3k.emulator.data + +import android.content.Context +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat + +data class UiLanguageOption( + val tag: String, + val label: String +) + +object UiLanguages { + private const val PREFS_NAME = "vita3k_frontend" + private const val PREF_UI_LANGUAGE = "ui_language" + + val options: List = listOf( + UiLanguageOption("", "System Default"), + UiLanguageOption("en", "English") + ) + + fun currentTag(context: Context): String = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString(PREF_UI_LANGUAGE, "") ?: "" + + fun applyStored(context: Context) { + apply(currentTag(context)) + } + + fun applyAndPersist(context: Context, tag: String) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putString(PREF_UI_LANGUAGE, tag) + .apply() + apply(tag) + } + + fun apply(tag: String) { + val locales = if (tag.isBlank()) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(tag) + } + AppCompatDelegate.setApplicationLocales(locales) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/data/UpdateCheckResult.kt b/android/app/src/main/java/org/vita3k/emulator/data/UpdateCheckResult.kt new file mode 100644 index 000000000..961ad0948 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/UpdateCheckResult.kt @@ -0,0 +1,31 @@ +package org.vita3k.emulator.data + +enum class UpdateCheckStatus { + Failed, + UpToDate, + UpdateAvailable, + CurrentBuildNewerThanLatest, + CustomBuildCanUpdate +} + +data class UpdateInfo( + // Human-readable release name or tag. + val version: String = "", + // Numeric build identifier parsed from the release metadata. + val buildNumber: Long = 0L, + // Browser URL for the release page. + val releaseUrl: String = "", + // GitHub publication timestamp in ISO-8601 format. + val publishedAt: String = "", + // Release notes with updater metadata lines removed. + val notes: String = "" +) + +data class UpdateCheckResult( + val status: UpdateCheckStatus, + val message: String, + // Parsed metadata for the latest available release. + val info: UpdateInfo = UpdateInfo(), + // Display version string for the installed build. + val currentDisplayVersion: String = "" +) diff --git a/android/app/src/main/java/org/vita3k/emulator/data/UserRepository.kt b/android/app/src/main/java/org/vita3k/emulator/data/UserRepository.kt new file mode 100644 index 000000000..7278db3fa --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/data/UserRepository.kt @@ -0,0 +1,24 @@ +package org.vita3k.emulator.data + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.vita3k.emulator.NativeLib + +internal object UserRepository { + + suspend fun getUsers(): List = withContext(Dispatchers.IO) { + NativeLib.getUsers().toList() + } + + suspend fun createUser(name: String): String = withContext(Dispatchers.IO) { + NativeLib.createUser(name) + } + + suspend fun activateUser(userId: String): Boolean = withContext(Dispatchers.IO) { + NativeLib.activateUser(userId) + } + + suspend fun deleteUser(userId: String): Boolean = withContext(Dispatchers.IO) { + NativeLib.deleteUser(userId) + } +} diff --git a/android/src/main/java/org/vita3k/emulator/overlay/InputOverlay.java b/android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlay.java similarity index 73% rename from android/src/main/java/org/vita3k/emulator/overlay/InputOverlay.java rename to android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlay.java index dfa20dac8..9779352be 100644 --- a/android/src/main/java/org/vita3k/emulator/overlay/InputOverlay.java +++ b/android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlay.java @@ -5,9 +5,6 @@ package org.vita3k.emulator.overlay; -import org.libsdl.app.SDL; -import org.libsdl.app.SDLActivity; -import org.vita3k.emulator.Emulator; import org.vita3k.emulator.R; import android.app.Activity; @@ -19,10 +16,8 @@ import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.preference.PreferenceManager; import android.util.AttributeSet; import android.util.DisplayMetrics; -import android.view.Display; import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; @@ -30,8 +25,6 @@ import android.view.View.OnTouchListener; import java.util.HashSet; import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; /** * Draws the interactive input overlay on top of the @@ -42,7 +35,11 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener // mirror what is in controller_dialog.cpp public final static int OVERLAY_MASK_BASIC = 1; public final static int OVERLAY_MASK_L2R2 = 2; - public final static int OVERLAY_MASK_TOUCH_SCREEN_SWITCH = 4; + public final static int OVERLAY_MASK_L3R3 = 4; + public final static int OVERLAY_MASK_TOUCH_SCREEN_SWITCH = 8; + public final static int OVERLAY_MASK_HIDE_TOGGLE = 16; + private final static int OVERLAY_MASK_UTILITY = + OVERLAY_MASK_TOUCH_SCREEN_SWITCH | OVERLAY_MASK_HIDE_TOGGLE; // wait 10 seconds without inputs before hiding private final static int OVERLAY_TIME_BEFORE_HIDE = 10; @@ -50,27 +47,32 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener private final Set overlayButtons = new HashSet<>(); private final Set overlayDpads = new HashSet<>(); private final Set overlayJoysticks = new HashSet<>(); - - private Rect mSurfacePosition = null; + private final Runnable mHideOverlayTicker = new Runnable() { + @Override + public void run() { + tick(); + postDelayed(this, 1000); + } + }; private int mOverlayMask = 0; private boolean mIsInEditMode = false; + private boolean mAutoHideEnabled = true; + private boolean mAllowVirtualController = true; + private boolean mControllerAttached = false; private InputOverlayDrawableButton mButtonBeingConfigured; private InputOverlayDrawableDpad mDpadBeingConfigured; private InputOverlayDrawableJoystick mJoystickBeingConfigured; private static float mGlobalScale = 1.0f; private static int mGlobalOpacity = 100; - - private Timer mTimer; + private String mLayoutProfileId = ""; // last Time the screen was touched private long mlastTouchTime; // is the overlay hidden because we didn't used it for long enough ? private boolean mShowingOverlay = true; - // hide overlay manually with touch overlay - private static boolean hide_overlay = false; - - private final SharedPreferences mPreferences; + // hide overlay manually while keeping the touch utility buttons available + private boolean mHideOverlayButtons = false; /** * Resizes a {@link Bitmap} by a given scale factor @@ -103,9 +105,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener { super(context/*, attrs*/); - mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (!mPreferences.getBoolean("OverlayInit", false)) - defaultOverlay(); + OverlayLayoutStore.ensureInitialized(getContext(), mLayoutProfileId); // Set the on touch listener. // Do not register the overlay as a touch listener @@ -118,39 +118,44 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener // Request focus for the overlay so it has priority on presses. requestFocus(); - /*SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - sPrefsEditor.putBoolean("OverlayInit", true); - sPrefsEditor.apply();*/ refreshControls(); - - mTimer = new Timer(); - - // call tick every second to check if we should stop displaying the overlay - mTimer.scheduleAtFixedRate(new TimerTask() - { - @Override - public void run() - { - Emulator emu = (Emulator) SDL.getContext(); - emu.getmOverlay().tick(); - } - }, 1000, 1000); - resetHideTimer(); } - private void resetHideTimer() - { - if (!mShowingOverlay) + private void startHideTimer() { + removeCallbacks(mHideOverlayTicker); + postDelayed(mHideOverlayTicker, 1000); + } + + private void stopHideTimer() { + removeCallbacks(mHideOverlayTicker); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + startHideTimer(); + updateVirtualControllerState(); + } + + @Override + protected void onDetachedFromWindow() { + stopHideTimer(); + releaseAllInputs(); + detachVirtualController(); + super.onDetachedFromWindow(); + } + + private void resetHideTimer(){ + if(!mShowingOverlay) invalidate(); mShowingOverlay = true; mlastTouchTime = System.currentTimeMillis(); } - public void tick() - { - if (mOverlayMask == 0 || !mShowingOverlay || isInEditMode()) + public void tick(){ + if(mOverlayMask == 0 || !mShowingOverlay || isInEditMode() || !mAutoHideEnabled) return; long current_time = System.currentTimeMillis(); @@ -160,42 +165,46 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener } } - public void setState(int overlay_mask) - { - boolean was_showing = mOverlayMask != 0; - - if (hide_overlay && mOverlayMask != 4) - { - mOverlayMask = 4; - refreshControls(); - invalidate(); - } - else if (mOverlayMask != overlay_mask) - { + public void setState(int overlay_mask){ + if(mOverlayMask != overlay_mask){ mOverlayMask = overlay_mask; invalidate(); } resetHideTimer(); + updateVirtualControllerState(); + } - boolean is_showing = overlay_mask != 0; - if (is_showing == was_showing) - return; + public int getOverlayMask() { + return mOverlayMask; + } - if (is_showing) - attachController(); - else - detachController(); + public void releaseAllInputs() { + for (InputOverlayDrawableButton button : overlayButtons) { + if ((button.getRole() & OVERLAY_MASK_UTILITY) == 0) { + setButton(button.getControl(), false); + } + button.setPressedState(false); + button.setTrackId(-1); + } + + for (InputOverlayDrawableDpad dpad : overlayDpads) { + for (int i = 0; i < 4; i++) { + setButton(dpad.getControl(i), false); + } + dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); + dpad.setTrackId(-1); + } + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { + setAxis(joystick.getXControl(), (short)0); + setAxis(joystick.getYControl(), (short)0); + joystick.release(); + } invalidate(); } - public void setSurfacePosition(Rect rect) - { - mSurfacePosition = rect; - } - - @Override public void draw(Canvas canvas) { super.draw(canvas); @@ -207,17 +216,17 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener { int role = button.getRole(); - if (hide_overlay) { - if (role == OVERLAY_MASK_TOUCH_SCREEN_SWITCH) - button.draw(canvas); - else - continue; - } else if ((role & mOverlayMask) != 0) { - button.draw(canvas); + if (mHideOverlayButtons) { + if ((role & OVERLAY_MASK_UTILITY) == 0 || (role & mOverlayMask) == 0) + continue; + } else if ((role & mOverlayMask) == 0) { + continue; } + + button.draw(canvas); } - if (!hide_overlay) + if (!mHideOverlayButtons) { for (InputOverlayDrawableDpad dpad : overlayDpads) { @@ -237,6 +246,9 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener if (mOverlayMask == 0) return false; + if(!mAllowVirtualController && !isInEditMode()) + return false; + resetHideTimer(); if (isInEditMode()) @@ -253,12 +265,13 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener for (InputOverlayDrawableButton button : overlayButtons) { - int btn_role = button.getRole(); - int legacy_id = button.getLegacyId(); + int buttonRole = button.getRole(); + int legacyId = button.getLegacyId(); - if (hide_overlay && btn_role != OVERLAY_MASK_TOUCH_SCREEN_SWITCH) + if (mHideOverlayButtons && + (((buttonRole & OVERLAY_MASK_UTILITY) == 0) || (buttonRole & mOverlayMask) == 0)) continue; - else if ((btn_role & mOverlayMask) == 0) + else if ((buttonRole & mOverlayMask) == 0) continue; // Determine the button state to apply based on the MotionEvent action flag. @@ -274,10 +287,10 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener button.setTrackId(event.getPointerId(pointerIndex)); concerned = true; - if (legacy_id == ButtonType.BUTTON_TOUCH_SWITCH) + if (legacyId == ButtonType.BUTTON_TOUCH_SWITCH) setTouchState(button.getPressed()); - else if (legacy_id == ButtonType.BUTTON_TOUCH_HIDE) - hide_overlay = !hide_overlay; + else if (legacyId == ButtonType.BUTTON_TOUCH_HIDE) + mHideOverlayButtons = !mHideOverlayButtons; else setButton(button.getControl(), true); } @@ -289,15 +302,9 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener { button.setPressedState(false); - if (legacy_id != ButtonType.BUTTON_TOUCH_SWITCH) + if (legacyId != ButtonType.BUTTON_TOUCH_SWITCH && + legacyId != ButtonType.BUTTON_TOUCH_HIDE) setButton(button.getControl(), false); - else if (legacy_id == ButtonType.BUTTON_TOUCH_HIDE && hide_overlay) - if (mOverlayMask != 4) { - mOverlayMask = 4; - detachController(); - refreshControls(); - attachController(); - } button.setTrackId(-1); concerned = true; @@ -306,7 +313,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener } } - if (!hide_overlay) + if (!mHideOverlayButtons) { for (InputOverlayDrawableDpad dpad : overlayDpads) { @@ -517,10 +524,6 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener return intersect; } - public void onDestroy() - { - } - private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left, boolean right) { @@ -557,85 +560,83 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener { overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_cross, R.drawable.button_cross_pressed, ButtonType.BUTTON_CROSS, ControlId.a, - orientation, OVERLAY_MASK_BASIC)); + orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_circle, R.drawable.button_circle_pressed, ButtonType.BUTTON_CIRCLE, ControlId.b, - orientation, OVERLAY_MASK_BASIC)); + orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_square, R.drawable.button_square_pressed, ButtonType.BUTTON_SQUARE, ControlId.x, - orientation, OVERLAY_MASK_BASIC)); + orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_triangle, R.drawable.button_triangle_pressed, ButtonType.BUTTON_TRIANGLE, ControlId.y, - orientation, OVERLAY_MASK_BASIC)); - + orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start, R.drawable.button_start_pressed, ButtonType.BUTTON_START, - ControlId.start, orientation, OVERLAY_MASK_BASIC)); + ControlId.start, orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_ps, R.drawable.button_ps_pressed, ButtonType.BUTTON_PS, - ControlId.guide, orientation, OVERLAY_MASK_BASIC)); + ControlId.guide, orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select, R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, - ControlId.select, orientation, OVERLAY_MASK_BASIC)); + ControlId.select, orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l, R.drawable.button_l_pressed, ButtonType.TRIGGER_L, - ControlId.l1, orientation, OVERLAY_MASK_BASIC)); - + ControlId.l1, orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r, R.drawable.button_r_pressed, ButtonType.TRIGGER_R, - ControlId.r1, orientation, OVERLAY_MASK_BASIC)); + ControlId.r1, orientation, OVERLAY_MASK_BASIC, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l2, R.drawable.button_l2_pressed, ButtonType.TRIGGER_L2, - ControlId.l2, orientation, OVERLAY_MASK_L2R2)); + ControlId.l2, orientation, OVERLAY_MASK_L2R2, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r2, R.drawable.button_r2_pressed, ButtonType.TRIGGER_R2, - ControlId.r2, orientation, OVERLAY_MASK_L2R2)); + ControlId.r2, orientation, OVERLAY_MASK_L2R2, mLayoutProfileId)); // L3 and R3 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l3, R.drawable.button_l3_pressed, ButtonType.TRIGGER_L3, - ControlId.l3, orientation, OVERLAY_MASK_L2R2)); + ControlId.l3, orientation, OVERLAY_MASK_L3R3, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r3, R.drawable.button_r3_pressed, ButtonType.TRIGGER_R3, - ControlId.r3, orientation, OVERLAY_MASK_L2R2)); + ControlId.r3, orientation, OVERLAY_MASK_L3R3, mLayoutProfileId)); overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_touch_f, R.drawable.button_touch_b, ButtonType.BUTTON_TOUCH_SWITCH, - ControlId.touch, orientation, OVERLAY_MASK_TOUCH_SCREEN_SWITCH)); + ControlId.touch, orientation, OVERLAY_MASK_TOUCH_SCREEN_SWITCH, mLayoutProfileId)); // show hide button overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_hide, R.drawable.button_hide_pressed, ButtonType.BUTTON_TOUCH_HIDE, - ControlId.touch, orientation, OVERLAY_MASK_TOUCH_SCREEN_SWITCH)); + ControlId.touch, orientation, OVERLAY_MASK_HIDE_TOGGLE, mLayoutProfileId)); overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad_idle, R.drawable.dpad_up, R.drawable.dpad_up_left, ButtonType.DPAD_UP, ControlId.dup, ControlId.ddown, - ControlId.dleft, ControlId.dright, orientation)); + ControlId.dleft, ControlId.dright, orientation, mLayoutProfileId)); overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.joystick_range, R.drawable.joystick, R.drawable.joystick_pressed, ButtonType.STICK_LEFT, ControlId.axis_left_x, - ControlId.axis_left_y, orientation)); - + ControlId.axis_left_y, orientation, mLayoutProfileId)); overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.joystick_range, R.drawable.joystick, R.drawable.joystick_pressed, ButtonType.STICK_RIGHT, ControlId.axis_right_x, - ControlId.axis_right_y, orientation)); + ControlId.axis_right_y, orientation, mLayoutProfileId)); } public void refreshControls() { + OverlayLayoutStore.ensureInitialized(getContext(), mLayoutProfileId); // Remove all the overlay buttons from the HashSet. overlayButtons.clear(); overlayDpads.clear(); @@ -649,7 +650,7 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener public void resetButtonPlacement() { - vitaDefaultOverlay(); + OverlayLayoutStore.resetToDefaults(getContext(), mLayoutProfileId); refreshControls(); } @@ -670,28 +671,13 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) { - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + final SharedPreferences sPrefs = OverlayLayoutStore.preferences(getContext()); SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); - sPrefsEditor.putFloat(getXKey(sharedPrefsId, orientation), x); - sPrefsEditor.putFloat(getYKey(sharedPrefsId, orientation), y); + sPrefsEditor.putFloat(OverlayLayoutStore.positionXKey(sharedPrefsId, orientation, mLayoutProfileId), x); + sPrefsEditor.putFloat(OverlayLayoutStore.positionYKey(sharedPrefsId, orientation, mLayoutProfileId), y); sPrefsEditor.apply(); } - private static String getKey(int sharedPrefsId, String orientation, String suffix) - { - return sharedPrefsId + orientation + suffix; - } - - private static String getXKey(int sharedPrefsId, String orientation) - { - return getKey(sharedPrefsId, orientation, "-X"); - } - - private static String getYKey(int sharedPrefsId, String orientation) - { - return getKey(sharedPrefsId, orientation, "-Y"); - } - /** * Initializes an InputOverlayDrawableButton, given by resId, with all of the * parameters set for it to be properly shown on the InputOverlay. @@ -724,13 +710,14 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. */ private static InputOverlayDrawableButton initializeOverlayButton(Context context, - int defaultResId, int pressedResId, int legacyId, int control, String orientation, int role) + int defaultResId, int pressedResId, int legacyId, int control, String orientation, int role, + String layoutProfileId) { // Resources handle for fetching the initial Drawable resource. final Resources res = context.getResources(); // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences sPrefs = OverlayLayoutStore.preferences(context); // Decide scale based on button ID and user preference float scale = 0.15f; @@ -760,8 +747,8 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(getXKey(legacyId, orientation), 0f); - int drawableY = (int) sPrefs.getFloat(getYKey(legacyId, orientation), 0f); + int drawableX = (int) sPrefs.getFloat(OverlayLayoutStore.positionXKey(legacyId, orientation, layoutProfileId), 0f); + int drawableY = (int) sPrefs.getFloat(OverlayLayoutStore.positionYKey(legacyId, orientation, layoutProfileId), 0f); int width = overlayDrawable.getWidth(); int height = overlayDrawable.getHeight(); @@ -800,13 +787,14 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener int downControl, int leftControl, int rightControl, - String orientation) + String orientation, + String layoutProfileId) { // Resources handle for fetching the initial Drawable resource. final Resources res = context.getResources(); // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences sPrefs = OverlayLayoutStore.preferences(context); // Decide scale based on button ID and user preference float scale = 0.35f; @@ -829,8 +817,8 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(getXKey(legacyId, orientation), 0f); - int drawableY = (int) sPrefs.getFloat(getYKey(legacyId, orientation), 0f); + int drawableX = (int) sPrefs.getFloat(OverlayLayoutStore.positionXKey(legacyId, orientation, layoutProfileId), 0f); + int drawableY = (int) sPrefs.getFloat(OverlayLayoutStore.positionYKey(legacyId, orientation, layoutProfileId), 0f); int width = overlayDrawable.getWidth(); int height = overlayDrawable.getHeight(); @@ -860,13 +848,13 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener */ private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, int resOuter, int defaultResInner, int pressedResInner, int legacyId, int xControl, - int yControl, String orientation) + int yControl, String orientation, String layoutProfileId) { // Resources handle for fetching the initial Drawable resource. final Resources res = context.getResources(); // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + final SharedPreferences sPrefs = OverlayLayoutStore.preferences(context); // Decide scale based on user preference float scale = 0.275f; @@ -880,8 +868,8 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(getXKey(legacyId, orientation), 0f); - int drawableY = (int) sPrefs.getFloat(getYKey(legacyId, orientation), 0f); + int drawableX = (int) sPrefs.getFloat(OverlayLayoutStore.positionXKey(legacyId, orientation, layoutProfileId), 0f); + int drawableY = (int) sPrefs.getFloat(OverlayLayoutStore.positionYKey(legacyId, orientation, layoutProfileId), 0f); // Decide inner scale based on joystick ID float innerScale = 1.375f; @@ -907,6 +895,10 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener public void setIsInEditMode(boolean isInEditMode) { mIsInEditMode = isInEditMode; + if (mIsInEditMode) { + mHideOverlayButtons = false; + mShowingOverlay = true; + } } public boolean isInEditMode() @@ -914,113 +906,62 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener return mIsInEditMode; } - private void defaultOverlay() + public void setAutoHideEnabled(boolean autoHideEnabled) { - if (!mPreferences.getBoolean("OverlayInit", false)) - { - vitaDefaultOverlay(); + mAutoHideEnabled = autoHideEnabled; + if(!mAutoHideEnabled){ + mShowingOverlay = true; + invalidate(); + } else { + resetHideTimer(); } } - private void vitaDefaultOverlay() + public void setAllowVirtualController(boolean allowVirtualController) { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + if(mAllowVirtualController == allowVirtualController) + return; - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for maxX. - if (maxY > maxX) - { - float tmp = maxX; - maxX = maxY; - maxY = tmp; + mAllowVirtualController = allowVirtualController; + updateVirtualControllerState(); + } + + public void setLayoutProfileId(String layoutProfileId) + { + String normalizedProfileId = OverlayLayoutStore.normalizeProfileId(layoutProfileId); + if(mLayoutProfileId.equals(normalizedProfileId)) + return; + + mLayoutProfileId = normalizedProfileId; + OverlayLayoutStore.ensureInitialized(getContext(), mLayoutProfileId); + refreshControls(); + } + + public void rebindController() + { + releaseAllInputs(); + detachVirtualController(); + updateVirtualControllerState(); + } + + private void updateVirtualControllerState() + { + boolean shouldAttach = mAllowVirtualController && mOverlayMask != 0 && getWindowToken() != null; + if (shouldAttach == mControllerAttached) + return; + + if (shouldAttach) { + attachController(); + mControllerAttached = true; + } else { + detachVirtualController(); } - Resources res = getResources(); + } - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.BUTTON_CROSS + "-X", - (((float) res.getInteger(R.integer.BUTTON_CROSS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_CROSS + "-Y", - (((float) res.getInteger(R.integer.BUTTON_CROSS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_CIRCLE + "-X", - (((float) res.getInteger(R.integer.BUTTON_CIRCLE_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_CIRCLE + "-Y", - (((float) res.getInteger(R.integer.BUTTON_CIRCLE_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SQUARE + "-X", - (((float) res.getInteger(R.integer.BUTTON_SQUARE_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SQUARE + "-Y", - (((float) res.getInteger(R.integer.BUTTON_SQUARE_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_TRIANGLE + "-X", - (((float) res.getInteger(R.integer.BUTTON_TRIANGLE_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_TRIANGLE + "-Y", - (((float) res.getInteger(R.integer.BUTTON_TRIANGLE_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", - (((float) res.getInteger(R.integer.BUTTON_SELECT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", - (((float) res.getInteger(R.integer.BUTTON_SELECT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", - (((float) res.getInteger(R.integer.BUTTON_START_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", - (((float) res.getInteger(R.integer.BUTTON_START_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_PS + "-X", - (((float) res.getInteger(R.integer.BUTTON_PS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_PS + "-Y", - (((float) res.getInteger(R.integer.BUTTON_PS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", - (((float) res.getInteger(R.integer.DPAD_UP_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", - (((float) res.getInteger(R.integer.DPAD_UP_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", - (((float) res.getInteger(R.integer.STICK_LEFT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", - (((float) res.getInteger(R.integer.STICK_LEFT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_RIGHT + "-X", - (((float) res.getInteger(R.integer.STICK_RIGHT_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_RIGHT + "-Y", - (((float) res.getInteger(R.integer.STICK_RIGHT_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", - (((float) res.getInteger(R.integer.TRIGGER_L_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_L_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", - (((float) res.getInteger(R.integer.TRIGGER_R_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_R_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L2 + "-X", - (((float) res.getInteger(R.integer.TRIGGER_L2_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L2 + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_L2_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R2 + "-X", - (((float) res.getInteger(R.integer.TRIGGER_R2_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R2 + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_R2_Y) / 1000) * maxY)); - - sPrefsEditor.putFloat(ButtonType.TRIGGER_L3 + "-X", - (((float) res.getInteger(R.integer.TRIGGER_L3_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L3 + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_L3_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R3 + "-X", - (((float) res.getInteger(R.integer.TRIGGER_R3_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R3 + "-Y", - (((float) res.getInteger(R.integer.TRIGGER_R3_Y) / 1000) * maxY)); - - sPrefsEditor.putFloat(ButtonType.BUTTON_TOUCH_SWITCH + "-X", - (((float) res.getInteger(R.integer.BUTTON_TOUCH_SWITCH_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_TOUCH_SWITCH + "-Y", - (((float) res.getInteger(R.integer.BUTTON_TOUCH_SWITCH_Y) / 1000) * maxY)); - - sPrefsEditor.putFloat(ButtonType.BUTTON_TOUCH_HIDE + "-X", - (((float) res.getInteger(R.integer.BUTTON_TOUCH_HIDE_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_TOUCH_HIDE + "-Y", - (((float) res.getInteger(R.integer.BUTTON_TOUCH_HIDE_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); + private void detachVirtualController() + { + detachController(); + mControllerAttached = false; } public native void attachController(); @@ -1075,7 +1016,6 @@ public final class InputOverlay extends SurfaceView implements OnTouchListener public static final int r2 = -5; // button to switch between front and back touch - public static final int hide = 1023; public static final int touch = 1024; public static final int axis_left_x = 0; diff --git a/android/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableButton.java b/android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableButton.java similarity index 100% rename from android/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableButton.java rename to android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableButton.java diff --git a/android/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableDpad.java b/android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableDpad.java similarity index 100% rename from android/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableDpad.java rename to android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableDpad.java diff --git a/android/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableJoystick.java b/android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableJoystick.java similarity index 95% rename from android/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableJoystick.java rename to android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableJoystick.java index a823252f5..0396ff121 100644 --- a/android/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableJoystick.java +++ b/android/app/src/main/java/org/vita3k/emulator/overlay/InputOverlayDrawableJoystick.java @@ -331,4 +331,19 @@ public final class InputOverlayDrawableJoystick { return trackId; } + + public void release() + { + mPressedState = false; + mCurrentX = mCurrentY = 0.0f; + mJoystickX = mJoystickY = 0.0f; + mOuterBitmap.setAlpha(mOpacity); + mBoundsBoxBitmap.setAlpha(0); + setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, + mOrigBounds.bottom)); + setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, + mOrigBounds.bottom)); + SetInnerBounds(); + trackId = -1; + } } diff --git a/android/app/src/main/java/org/vita3k/emulator/overlay/OverlayConfig.kt b/android/app/src/main/java/org/vita3k/emulator/overlay/OverlayConfig.kt new file mode 100644 index 000000000..bac02790e --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/overlay/OverlayConfig.kt @@ -0,0 +1,117 @@ +package org.vita3k.emulator.overlay + +import android.content.Context + +val DEFAULT_OVERLAY_MASK: Int = + InputOverlay.OVERLAY_MASK_BASIC or InputOverlay.OVERLAY_MASK_TOUCH_SCREEN_SWITCH + +data class OverlayConfig( + val overlayMask: Int = DEFAULT_OVERLAY_MASK, + val overlayScale: Int = 100, + val overlayOpacity: Int = 100, + val hideOverlayWhenControllerConnected: Boolean = true +) { + val controlsVisible: Boolean + get() = overlayMask != 0 + + val l2r2Visible: Boolean + get() = overlayMask and InputOverlay.OVERLAY_MASK_L2R2 != 0 + + val l3r3Visible: Boolean + get() = overlayMask and InputOverlay.OVERLAY_MASK_L3R3 != 0 + + val touchSwitchVisible: Boolean + get() = overlayMask and InputOverlay.OVERLAY_MASK_TOUCH_SCREEN_SWITCH != 0 + + val hideToggleVisible: Boolean + get() = overlayMask and InputOverlay.OVERLAY_MASK_HIDE_TOGGLE != 0 + + fun activeMask(): Int = if (overlayMask == 0) DEFAULT_OVERLAY_MASK else overlayMask + + fun withControlsVisible(visible: Boolean): OverlayConfig { + return copy(overlayMask = if (visible) activeMask() else 0).normalized() + } + + fun withL2R2Visible(visible: Boolean): OverlayConfig { + val baseMask = activeMask() + val updatedMask = if (visible) { + baseMask or InputOverlay.OVERLAY_MASK_L2R2 + } else { + baseMask and InputOverlay.OVERLAY_MASK_L2R2.inv() + } + return copy(overlayMask = updatedMask).normalized() + } + + fun withL3R3Visible(visible: Boolean): OverlayConfig { + val baseMask = activeMask() + val updatedMask = if (visible) { + baseMask or InputOverlay.OVERLAY_MASK_L3R3 + } else { + baseMask and InputOverlay.OVERLAY_MASK_L3R3.inv() + } + return copy(overlayMask = updatedMask).normalized() + } + + fun withTouchSwitchVisible(visible: Boolean): OverlayConfig { + val baseMask = activeMask() + val updatedMask = if (visible) { + baseMask or InputOverlay.OVERLAY_MASK_TOUCH_SCREEN_SWITCH + } else { + baseMask and InputOverlay.OVERLAY_MASK_TOUCH_SCREEN_SWITCH.inv() + } + return copy(overlayMask = updatedMask).normalized() + } + + fun withHideToggleVisible(visible: Boolean): OverlayConfig { + val baseMask = activeMask() + val updatedMask = if (visible) { + baseMask or InputOverlay.OVERLAY_MASK_HIDE_TOGGLE + } else { + baseMask and InputOverlay.OVERLAY_MASK_HIDE_TOGGLE.inv() + } + return copy(overlayMask = updatedMask).normalized() + } + + fun normalized(): OverlayConfig { + val knownMask = InputOverlay.OVERLAY_MASK_BASIC or + InputOverlay.OVERLAY_MASK_L2R2 or + InputOverlay.OVERLAY_MASK_L3R3 or + InputOverlay.OVERLAY_MASK_TOUCH_SCREEN_SWITCH or + InputOverlay.OVERLAY_MASK_HIDE_TOGGLE + return copy( + overlayMask = overlayMask.coerceAtLeast(0) and knownMask, + overlayScale = overlayScale.coerceIn(50, 150), + overlayOpacity = overlayOpacity.coerceIn(20, 100) + ) + } +} + +object OverlayConfigStore { + private const val PREFS_NAME = "emulation_session" + private const val KEY_OVERLAY_MASK = "overlay_mask" + private const val KEY_OVERLAY_SCALE = "overlay_scale" + private const val KEY_OVERLAY_OPACITY = "overlay_opacity" + private const val KEY_HIDE_ON_CONTROLLER = "hide_overlay_on_controller" + + fun load(context: Context): OverlayConfig { + val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + return OverlayConfig( + overlayMask = prefs.getInt(KEY_OVERLAY_MASK, DEFAULT_OVERLAY_MASK), + overlayScale = prefs.getInt(KEY_OVERLAY_SCALE, 100), + overlayOpacity = prefs.getInt(KEY_OVERLAY_OPACITY, 100), + hideOverlayWhenControllerConnected = prefs.getBoolean(KEY_HIDE_ON_CONTROLLER, true) + ).normalized() + } + + fun save(context: Context, config: OverlayConfig) { + val normalized = config.normalized() + context.applicationContext + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putInt(KEY_OVERLAY_MASK, normalized.overlayMask) + .putInt(KEY_OVERLAY_SCALE, normalized.overlayScale) + .putInt(KEY_OVERLAY_OPACITY, normalized.overlayOpacity) + .putBoolean(KEY_HIDE_ON_CONTROLLER, normalized.hideOverlayWhenControllerConnected) + .apply() + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/overlay/OverlayLayoutStore.kt b/android/app/src/main/java/org/vita3k/emulator/overlay/OverlayLayoutStore.kt new file mode 100644 index 000000000..4e56efb60 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/overlay/OverlayLayoutStore.kt @@ -0,0 +1,124 @@ +package org.vita3k.emulator.overlay + +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.util.DisplayMetrics +import org.vita3k.emulator.R + +object OverlayLayoutStore { + private const val KEY_OVERLAY_INIT = "OverlayInit" + + private data class OverlayPositionDefaults( + val buttonType: Int, + val xResId: Int, + val yResId: Int + ) + + private val defaultEntries = listOf( + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_CROSS, R.integer.BUTTON_CROSS_X, R.integer.BUTTON_CROSS_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_CIRCLE, R.integer.BUTTON_CIRCLE_X, R.integer.BUTTON_CIRCLE_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_SQUARE, R.integer.BUTTON_SQUARE_X, R.integer.BUTTON_SQUARE_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_TRIANGLE, R.integer.BUTTON_TRIANGLE_X, R.integer.BUTTON_TRIANGLE_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_SELECT, R.integer.BUTTON_SELECT_X, R.integer.BUTTON_SELECT_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_START, R.integer.BUTTON_START_X, R.integer.BUTTON_START_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_PS, R.integer.BUTTON_PS_X, R.integer.BUTTON_PS_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.DPAD_UP, R.integer.DPAD_UP_X, R.integer.DPAD_UP_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.STICK_LEFT, R.integer.STICK_LEFT_X, R.integer.STICK_LEFT_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.STICK_RIGHT, R.integer.STICK_RIGHT_X, R.integer.STICK_RIGHT_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.TRIGGER_L, R.integer.TRIGGER_L_X, R.integer.TRIGGER_L_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.TRIGGER_R, R.integer.TRIGGER_R_X, R.integer.TRIGGER_R_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.TRIGGER_L2, R.integer.TRIGGER_L2_X, R.integer.TRIGGER_L2_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.TRIGGER_R2, R.integer.TRIGGER_R2_X, R.integer.TRIGGER_R2_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.TRIGGER_L3, R.integer.TRIGGER_L3_X, R.integer.TRIGGER_L3_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.TRIGGER_R3, R.integer.TRIGGER_R3_X, R.integer.TRIGGER_R3_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_TOUCH_HIDE, R.integer.BUTTON_TOUCH_HIDE_X, R.integer.BUTTON_TOUCH_HIDE_Y), + OverlayPositionDefaults(InputOverlay.ButtonType.BUTTON_TOUCH_SWITCH, R.integer.BUTTON_TOUCH_SWITCH_X, R.integer.BUTTON_TOUCH_SWITCH_Y) + ) + + @JvmStatic + fun ensureInitialized(context: Context, profileId: String? = null) { + val prefs = preferences(context) + val initKey = initKey(profileId) + if (!prefs.getBoolean(initKey, false)) { + resetToDefaults(context, profileId) + } + } + + @JvmStatic + fun resetToDefaults(context: Context, profileId: String? = null) { + val prefs = preferences(context) + val editor = prefs.edit() + val resources = context.resources + val (maxX, maxY) = resolveOverlayBounds(context) + + defaultEntries.forEach { entry -> + editor.putFloat( + positionXKey(entry.buttonType, "", profileId), + (resources.getInteger(entry.xResId) / 1000f) * maxX + ) + editor.putFloat( + positionYKey(entry.buttonType, "", profileId), + (resources.getInteger(entry.yResId) / 1000f) * maxY + ) + } + + editor.putBoolean(initKey(profileId), true) + editor.apply() + } + + @JvmStatic + fun positionXKey(buttonType: Int, orientation: String, profileId: String? = null): String = + scopedKey("$buttonType$orientation-X", profileId) + + @JvmStatic + fun positionYKey(buttonType: Int, orientation: String, profileId: String? = null): String = + scopedKey("$buttonType$orientation-Y", profileId) + + @JvmStatic + fun initKey(profileId: String? = null): String = scopedKey(KEY_OVERLAY_INIT, profileId) + + @JvmStatic + fun normalizeProfileId(profileId: String?): String = profileId.orEmpty().trim() + + @JvmStatic + fun preferences(context: Context): SharedPreferences = + context.getSharedPreferences("${context.packageName}_preferences", Context.MODE_PRIVATE) + + private fun scopedKey(rawKey: String, profileId: String?): String { + val normalizedProfile = normalizeProfileId(profileId) + return if (normalizedProfile.isEmpty()) rawKey else "overlay:$normalizedProfile:$rawKey" + } + + private fun resolveOverlayBounds(context: Context): Pair { + val activity = context as? Activity + if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val bounds = activity.windowManager.currentWindowMetrics.bounds + val width = bounds.width().toFloat() + val height = bounds.height().toFloat() + return if (width >= height) { + width to height + } else { + height to width + } + } + + val metrics = DisplayMetrics() + if (activity != null) { + @Suppress("DEPRECATION") + activity.windowManager.defaultDisplay.getMetrics(metrics) + } else { + metrics.setTo(context.resources.displayMetrics) + } + + var maxX = metrics.heightPixels.toFloat() + var maxY = metrics.widthPixels.toFloat() + if (maxY > maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + return maxX to maxY + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/AndroidUi.kt b/android/app/src/main/java/org/vita3k/emulator/ui/AndroidUi.kt new file mode 100644 index 000000000..797c259f9 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/AndroidUi.kt @@ -0,0 +1,42 @@ +package org.vita3k.emulator.ui + +import android.content.Context +import android.hardware.input.InputManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.vita3k.emulator.ConnectedGamepad +import org.vita3k.emulator.InputDeviceUtils + +const val CUSTOM_DRIVER_DOWNLOAD_URL = "https://github.com/K11MCH1/AdrenoToolsDrivers/releases/" + +@Composable +fun rememberConnectedGamepads(context: Context): List { + var connectedGamepads by remember { mutableStateOf(InputDeviceUtils.getPhysicalGamepads()) } + + DisposableEffect(context) { + val inputManager = context.getSystemService(Context.INPUT_SERVICE) as? InputManager + val listener = object : InputManager.InputDeviceListener { + private fun refresh() { + connectedGamepads = InputDeviceUtils.getPhysicalGamepads() + } + + override fun onInputDeviceAdded(deviceId: Int) = refresh() + + override fun onInputDeviceRemoved(deviceId: Int) = refresh() + + override fun onInputDeviceChanged(deviceId: Int) = refresh() + } + + connectedGamepads = InputDeviceUtils.getPhysicalGamepads() + inputManager?.registerInputDeviceListener(listener, null) + onDispose { + inputManager?.unregisterInputDeviceListener(listener) + } + } + + return connectedGamepads +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/components/HtmlText.kt b/android/app/src/main/java/org/vita3k/emulator/ui/components/HtmlText.kt new file mode 100644 index 000000000..13d569cc4 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/components/HtmlText.kt @@ -0,0 +1,48 @@ +package org.vita3k.emulator.ui.components + +import android.text.method.LinkMovementMethod +import android.view.Gravity +import android.view.View +import android.widget.TextView +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.text.HtmlCompat + +@Composable +fun HtmlText( + html: String, + modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.bodyMedium, + textColor: Color = MaterialTheme.colorScheme.onSurface, + linkColor: Color = MaterialTheme.colorScheme.primary, + gravity: Int = Gravity.START +) { + AndroidView( + modifier = modifier, + factory = { context -> + TextView(context).apply { + movementMethod = LinkMovementMethod.getInstance() + highlightColor = android.graphics.Color.TRANSPARENT + setBackgroundColor(android.graphics.Color.TRANSPARENT) + linksClickable = true + } + }, + update = { view -> + view.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) + view.setTextColor(textColor.toArgb()) + view.setLinkTextColor(linkColor.toArgb()) + view.textSize = textStyle.fontSize.value + view.gravity = gravity + view.textAlignment = if (gravity == Gravity.CENTER) { + View.TEXT_ALIGNMENT_CENTER + } else { + View.TEXT_ALIGNMENT_VIEW_START + } + } + ) +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/components/OverlayEditorPalette.kt b/android/app/src/main/java/org/vita3k/emulator/ui/components/OverlayEditorPalette.kt new file mode 100644 index 000000000..63927b615 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/components/OverlayEditorPalette.kt @@ -0,0 +1,105 @@ +package org.vita3k.emulator.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Restore +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R +import kotlin.math.roundToInt + +@Composable +internal fun OverlayEditorPalette( + onDone: () -> Unit, + onReset: () -> Unit, + modifier: Modifier = Modifier +) { + BoxWithConstraints( + modifier = modifier.fillMaxSize() + ) { + var offsetX by rememberSaveable { mutableIntStateOf(0) } + var offsetY by rememberSaveable { mutableIntStateOf(0) } + var panelSize by remember { mutableStateOf(IntSize.Zero) } + + val maxOffsetX = ((constraints.maxWidth - panelSize.width) / 2).coerceAtLeast(0) + val maxOffsetY = ((constraints.maxHeight - panelSize.height) / 2).coerceAtLeast(0) + + Surface( + modifier = Modifier + .align(Alignment.Center) + .offset { IntOffset(offsetX, offsetY) } + .widthIn(min = 220.dp, max = 280.dp) + .onSizeChanged { panelSize = it } + .pointerInput(maxOffsetX, maxOffsetY) { + detectDragGestures { change, dragAmount -> + change.consume() + offsetX = (offsetX + dragAmount.x.roundToInt()) + .coerceIn(-maxOffsetX, maxOffsetX) + offsetY = (offsetY + dragAmount.y.roundToInt()) + .coerceIn(-maxOffsetY, maxOffsetY) + } + }, + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + OutlinedButton( + onClick = onReset, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Restore, contentDescription = null) + Text( + text = stringResource(R.string.action_reset), + modifier = Modifier.padding(start = 6.dp) + ) + } + FilledTonalButton( + onClick = onDone, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Done, contentDescription = null) + Text( + text = stringResource(R.string.action_done), + modifier = Modifier.padding(start = 6.dp) + ) + } + } + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/navigation/AppNavigation.kt b/android/app/src/main/java/org/vita3k/emulator/ui/navigation/AppNavigation.kt new file mode 100644 index 000000000..907bfcc76 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/navigation/AppNavigation.kt @@ -0,0 +1,349 @@ +package org.vita3k.emulator.ui.navigation + +import android.net.Uri +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.material3.CircularProgressIndicator +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import org.vita3k.emulator.MainActivity +import org.vita3k.emulator.data.AppStorage +import org.vita3k.emulator.data.AppInfo +import org.vita3k.emulator.ui.screens.AppInfoSheet +import org.vita3k.emulator.ui.screens.AppsListScreen +import org.vita3k.emulator.ui.screens.InitialSetupScreen +import org.vita3k.emulator.ui.screens.ArchiveInstallSourceDialog +import org.vita3k.emulator.ui.screens.InstallBottomSheet +import org.vita3k.emulator.ui.screens.InstallProgressDialog +import org.vita3k.emulator.ui.screens.InstallResultDialog +import org.vita3k.emulator.ui.screens.LicenseSourceDialog +import org.vita3k.emulator.ui.screens.UserManagementScreen +import org.vita3k.emulator.ui.screens.settings.SettingsRoute +import org.vita3k.emulator.ui.viewmodel.AppsListViewModel +import org.vita3k.emulator.ui.viewmodel.InstallViewModel +import org.vita3k.emulator.ui.viewmodel.InstallType +import org.vita3k.emulator.ui.viewmodel.SettingsViewModel +import org.vita3k.emulator.ui.viewmodel.UserManagementViewModel + +private const val ROUTE_INITIAL_SETUP = "initial_setup" +private const val ROUTE_INITIAL_SETUP_MANUAL = "initial_setup/manual" +private const val ROUTE_APPS_LIST = "apps_list" +private const val ROUTE_SETTINGS = "settings" +private const val ROUTE_USER_MANAGEMENT = "users" +private const val ROUTE_CUSTOM_CONFIG = "settings/custom/{titleId}?appName={appName}" +private const val ARG_TITLE_ID = "titleId" +private const val ARG_APP_NAME = "appName" +private val FIRMWARE_EXTENSIONS = setOf(".pup") +private val PKG_EXTENSIONS = setOf(".pkg") +private val ARCHIVE_EXTENSIONS = setOf(".zip", ".vpk") +private val LICENSE_EXTENSIONS = setOf(".bin", ".rif") + +private fun customConfigRoute(titleId: String, appName: String): String { + return "settings/custom/${Uri.encode(titleId)}?appName=${Uri.encode(appName)}" +} + +@Composable +fun AppNavigation( + appsListViewModel: AppsListViewModel, + installViewModel: InstallViewModel, + settingsViewModel: SettingsViewModel, + userManagementViewModel: UserManagementViewModel, + onAppLaunch: (AppInfo) -> Unit +) { + val navController = rememberNavController() + val context = LocalContext.current + val activity = context as? MainActivity + var showArchiveSourceDialog by remember { mutableStateOf(false) } + var showStandaloneLicenseDialog by remember { mutableStateOf(false) } + val startDestination by produceState(initialValue = null, key1 = context) { + value = if (AppStorage.isInitialSetupCompleted(context)) ROUTE_APPS_LIST else ROUTE_INITIAL_SETUP + } + + LaunchedEffect(appsListViewModel.initialized) { + if (appsListViewModel.initialized) { + settingsViewModel.preloadGlobalSettings() + } + } + + LaunchedEffect(installViewModel.completedInstallToken) { + if (installViewModel.completedInstallToken != 0L) { + appsListViewModel.reloadAppsList() + settingsViewModel.load(titleId = null, force = true) + } + } + + fun requestInstallPicker(type: InstallType) { + val hostActivity = activity ?: return + when (type) { + InstallType.FIRMWARE -> hostActivity.requestInstallFilePath(FIRMWARE_EXTENSIONS) { path -> + path?.let { installViewModel.installFirmware(it) } + } + InstallType.PKG -> hostActivity.requestInstallFilePath(PKG_EXTENSIONS) { path -> + path?.let { installViewModel.onPkgPicked(it) } + } + InstallType.ARCHIVE -> hostActivity.requestInstallFilePath(ARCHIVE_EXTENSIONS) { path -> + path?.let { installViewModel.installArchive(it) } + } + InstallType.LICENSE -> hostActivity.requestInstallFilePath(LICENSE_EXTENSIONS) { path -> + path?.let { installViewModel.installLicense(it) } + } + } + } + + if (installViewModel.showPkgLicenseDialog) { + LicenseSourceDialog( + title = stringResource(org.vita3k.emulator.R.string.license_required_title), + message = stringResource(org.vita3k.emulator.R.string.license_required_message), + onSelectLicenseFile = { + activity?.requestInstallFilePath(LICENSE_EXTENSIONS) { path -> + if (path != null) { + installViewModel.onPkgLicenseFilePicked(path) + } else { + installViewModel.cancelPkgInstall() + } + } + }, + onEnterZrif = { zrif -> installViewModel.confirmPkgInstall(zrif) }, + onDismiss = { installViewModel.cancelPkgInstall() } + ) + } + + if (showArchiveSourceDialog) { + ArchiveInstallSourceDialog( + onSelectFile = { + showArchiveSourceDialog = false + requestInstallPicker(InstallType.ARCHIVE) + }, + onSelectFolder = { + showArchiveSourceDialog = false + activity?.requestArchiveFolderPaths(ARCHIVE_EXTENSIONS) { paths -> + if (paths.isNotEmpty()) { + installViewModel.installArchiveFolder(paths) + } + } + }, + onDismiss = { showArchiveSourceDialog = false } + ) + } + + if (showStandaloneLicenseDialog) { + LicenseSourceDialog( + title = stringResource(org.vita3k.emulator.R.string.install_license_source_title), + message = stringResource(org.vita3k.emulator.R.string.install_license_source_message), + onSelectLicenseFile = { + showStandaloneLicenseDialog = false + activity?.requestInstallFilePath(LICENSE_EXTENSIONS) { path -> + path?.let { installViewModel.installLicense(it) } + } + }, + onEnterZrif = { zrif -> + showStandaloneLicenseDialog = false + installViewModel.installLicenseFromZrif(zrif) + }, + onDismiss = { showStandaloneLicenseDialog = false } + ) + } + + if (installViewModel.showInstallSheet) { + InstallBottomSheet( + onDismiss = { installViewModel.hideSheet() }, + onSelectType = { + installViewModel.hideSheet() + when (it) { + InstallType.ARCHIVE -> showArchiveSourceDialog = true + InstallType.LICENSE -> showStandaloneLicenseDialog = true + else -> requestInstallPicker(it) + } + } + ) + } + + if (installViewModel.installing) { + InstallProgressDialog( + progress = installViewModel.progress, + statusMessage = installViewModel.statusMessage + ) + } + + installViewModel.installResult?.let { result -> + InstallResultDialog( + result = result, + onConfirm = { selectedOptions -> installViewModel.confirmInstallResult(selectedOptions) } + ) + } + + appsListViewModel.infoDialogApp?.let { app -> + AppInfoSheet( + app = app, + installSizeBytes = appsListViewModel.infoAppInstallSizeBytes, + onDismiss = { appsListViewModel.dismissAppInfo() } + ) + } + + if (startDestination == null) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + return + } + + NavHost( + navController = navController, + startDestination = startDestination!!, + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None }, + popEnterTransition = { EnterTransition.None }, + popExitTransition = { ExitTransition.None } + ) { + composable(ROUTE_INITIAL_SETUP) { + val completeSetup: () -> Unit = { + AppStorage.setInitialSetupCompleted(context, true) + navController.navigate(ROUTE_APPS_LIST) { + launchSingleTop = true + popUpTo(ROUTE_INITIAL_SETUP) { inclusive = true } + } + } + + InitialSetupScreen( + firmwareInstallState = appsListViewModel.firmwareInstallState, + preferredLanguageIndex = settingsViewModel.config.sysLang, + onInstallFirmware = { requestInstallPicker(InstallType.FIRMWARE) }, + dismissLabel = stringResource(org.vita3k.emulator.R.string.initial_setup_skip), + onSkip = completeSetup, + onFinish = completeSetup + ) + } + + composable(ROUTE_INITIAL_SETUP_MANUAL) { + InitialSetupScreen( + firmwareInstallState = appsListViewModel.firmwareInstallState, + preferredLanguageIndex = settingsViewModel.config.sysLang, + onInstallFirmware = { requestInstallPicker(InstallType.FIRMWARE) }, + dismissLabel = stringResource(org.vita3k.emulator.R.string.emulation_exit), + onSkip = { navController.popBackStack() }, + onFinish = { navController.popBackStack() } + ) + } + + composable(ROUTE_APPS_LIST) { + AppsListScreen( + apps = appsListViewModel.apps, + initialized = appsListViewModel.initialized, + loading = appsListViewModel.loading, + appVersion = appsListViewModel.appVersion, + searchQuery = appsListViewModel.searchQuery, + sortOption = appsListViewModel.sortOption, + viewMode = appsListViewModel.viewMode, + updateCheckInProgress = appsListViewModel.updateCheckInProgress, + updateCheckResult = appsListViewModel.updateCheckResult, + firmwareInstallState = appsListViewModel.firmwareInstallState, + actionInProgress = appsListViewModel.actionInProgress, + actionResultMessage = appsListViewModel.actionResultMessage, + selectionMode = appsListViewModel.selectionMode, + selectedAppIds = appsListViewModel.selectedAppIds, + resolveAvailableActions = { app -> appsListViewModel.getAvailableActions(app.titleId) }, + onAppSelected = { app -> onAppLaunch(app) }, + onRunAppAction = { app, action -> appsListViewModel.runAppAction(app, action) }, + onPrepareAppActions = { app -> appsListViewModel.prepareAppActions(app) }, + onShowAppInfo = { app -> appsListViewModel.showAppInfo(app) }, + onToggleAppSelection = { app -> appsListViewModel.toggleAppSelection(app) }, + onSetSelectionMode = { enabled -> appsListViewModel.updateSelectionMode(enabled) }, + onSelectAllVisible = { appsListViewModel.selectAllVisibleApps() }, + onRunBatchDeleteSelected = { appsListViewModel.runBatchDeleteSelected() }, + onDismissActionResult = { appsListViewModel.dismissActionResult() }, + onSearchChanged = { appsListViewModel.setSearch(it) }, + onSortChanged = { appsListViewModel.setSort(it) }, + onViewModeToggle = { appsListViewModel.toggleViewMode() }, + onCheckForUpdates = { appsListViewModel.checkForUpdates() }, + onDismissUpdateCheckResult = { appsListViewModel.dismissUpdateCheckResult() }, + onRefresh = { appsListViewModel.refreshAppsList(syncCompatibility = true) }, + onInstallClick = { installViewModel.showSheet() }, + onOpenSettings = { + navController.navigate(ROUTE_SETTINGS) { + launchSingleTop = true + } + }, + onOpenUserManagement = { + navController.navigate(ROUTE_USER_MANAGEMENT) { + launchSingleTop = true + } + }, + onOpenWelcomeScreen = { + navController.navigate(ROUTE_INITIAL_SETUP_MANUAL) { + launchSingleTop = true + } + }, + onOpenCustomConfig = { app -> + navController.navigate(customConfigRoute(app.titleId, app.title)) { + launchSingleTop = true + } + } + ) + } + + composable(ROUTE_SETTINGS) { + SettingsRoute( + titleId = null, + appName = null, + viewModel = settingsViewModel, + onStorageChanged = { appsListViewModel.refreshAppsList() }, + onBack = { navController.popBackStack() } + ) + } + + composable(ROUTE_USER_MANAGEMENT) { + UserManagementScreen( + viewModel = userManagementViewModel, + onBack = { navController.popBackStack() }, + onUsersChanged = { appsListViewModel.refreshAppsList() } + ) + } + + composable( + route = ROUTE_CUSTOM_CONFIG, + arguments = listOf( + navArgument(ARG_TITLE_ID) { type = NavType.StringType }, + navArgument(ARG_APP_NAME) { + type = NavType.StringType + defaultValue = "" + } + ) + ) { backStackEntry -> + val titleId = backStackEntry.arguments?.getString(ARG_TITLE_ID).orEmpty() + val appName = backStackEntry.arguments?.getString(ARG_APP_NAME) + ?.takeIf { it.isNotBlank() } + ?: appsListViewModel.apps.firstOrNull { it.titleId == titleId }?.title + ?: titleId + + SettingsRoute( + titleId = titleId, + appName = appName, + viewModel = settingsViewModel, + onStorageChanged = { appsListViewModel.refreshAppsList() }, + onBack = { + appsListViewModel.refreshAppsList() + navController.popBackStack() + } + ) + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/AppInfoSheet.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/AppInfoSheet.kt new file mode 100644 index 000000000..43bac4950 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/AppInfoSheet.kt @@ -0,0 +1,150 @@ +package org.vita3k.emulator.ui.screens + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R +import org.vita3k.emulator.data.AppInfo +import org.vita3k.emulator.ui.theme.SCRIM_ALPHA +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppInfoSheet( + app: AppInfo, + installSizeBytes: Long, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + scrimColor = Color.Black.copy(alpha = SCRIM_ALPHA) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(stringResource(R.string.app_info_title), style = MaterialTheme.typography.titleLarge) + IconButton(onClick = onDismiss) { + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.app_info_cd_close)) + } + } + + Row( + modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Top + ) { + AppIcon(app = app, size = 80) + Column { + Text(app.title, style = MaterialTheme.typography.titleMedium) + Text( + app.titleId, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + CompatBadge(app) + } + } + + HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) + + InfoRow(label = stringResource(R.string.app_info_field_title), value = app.title) + InfoRow(label = stringResource(R.string.app_info_field_serial), value = app.titleId) + InfoRow(label = stringResource(R.string.app_info_field_app_version), value = app.appVer.ifEmpty { "\u2014" }) + InfoRow(label = stringResource(R.string.app_info_field_category), value = app.category.ifEmpty { "\u2014" }) + InfoRow( + label = stringResource(R.string.app_info_field_install_size), + value = when { + installSizeBytes == -1L -> stringResource(R.string.app_info_size_calculating) + installSizeBytes == 0L -> stringResource(R.string.app_info_size_unknown) + installSizeBytes < 1024L * 1024L -> + stringResource(R.string.app_info_size_kb, (installSizeBytes / 1024L).toInt()) + installSizeBytes < 1024L * 1024L * 1024L -> + stringResource(R.string.app_info_size_mb, (installSizeBytes / (1024.0 * 1024.0)).toFloat()) + else -> + stringResource(R.string.app_info_size_gb, (installSizeBytes / (1024.0 * 1024.0 * 1024.0)).toFloat()) + } + ) + InfoRow(label = stringResource(R.string.app_info_field_play_time), value = formatPlaytime(app.playtime)) + InfoRow(label = stringResource(R.string.app_info_field_last_played), value = formatLastPlayed(app.lastPlayed)) + + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun formatPlaytime(seconds: Long): String { + if (seconds <= 0) return stringResource(R.string.app_info_playtime_never) + val h = seconds / 3600 + val m = (seconds % 3600) / 60 + val s = seconds % 60 + return when { + h > 0 -> stringResource(R.string.app_info_playtime_hm, h.toInt(), m.toInt()) + m > 0 -> stringResource(R.string.app_info_playtime_ms, m.toInt(), s.toInt()) + else -> stringResource(R.string.app_info_playtime_s, s.toInt()) + } +} + +@Composable +private fun formatLastPlayed(timestampSecs: Long): String { + if (timestampSecs <= 0) return stringResource(R.string.app_info_last_played_never) + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .withZone(ZoneId.systemDefault()) + return formatter.format(Instant.ofEpochSecond(timestampSecs)) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun InfoRow(label: String, value: String) { + val clipboardManager = LocalClipboardManager.current + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = {}, + onLongClick = { clipboardManager.setText(AnnotatedString(value)) } + ) + .padding(horizontal = 24.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text(value, style = MaterialTheme.typography.bodyMedium) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/AppsListScreen.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/AppsListScreen.kt new file mode 100644 index 000000000..e1d8e099e --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/AppsListScreen.kt @@ -0,0 +1,1379 @@ +package org.vita3k.emulator.ui.screens + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.view.Gravity +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.SystemUpdate +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import coil.compose.AsyncImage +import org.vita3k.emulator.R +import org.vita3k.emulator.data.FirmwareInstallState +import org.vita3k.emulator.data.AppInfo +import org.vita3k.emulator.data.FirmwareComponent +import org.vita3k.emulator.data.SortOption +import org.vita3k.emulator.data.UpdateCheckResult +import org.vita3k.emulator.data.UpdateCheckStatus +import org.vita3k.emulator.data.ViewMode +import org.vita3k.emulator.ui.components.HtmlText +import org.vita3k.emulator.ui.theme.ApplyDialogDim +import org.vita3k.emulator.ui.theme.SCRIM_ALPHA +import org.vita3k.emulator.ui.viewmodel.AppAction +import org.vita3k.emulator.ui.viewmodel.AppActionGroup +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppsListScreen( + apps: List, + initialized: Boolean, + loading: Boolean, + appVersion: String, + searchQuery: String, + sortOption: SortOption, + viewMode: ViewMode, + updateCheckInProgress: Boolean, + updateCheckResult: UpdateCheckResult?, + firmwareInstallState: FirmwareInstallState, + actionInProgress: Boolean, + actionResultMessage: String?, + selectionMode: Boolean, + selectedAppIds: Set, + resolveAvailableActions: (AppInfo) -> Set, + onAppSelected: (AppInfo) -> Unit, + onRunAppAction: (AppInfo, AppAction) -> Unit, + onPrepareAppActions: (AppInfo) -> Unit, + onShowAppInfo: (AppInfo) -> Unit, + onToggleAppSelection: (AppInfo) -> Unit, + onSetSelectionMode: (Boolean) -> Unit, + onSelectAllVisible: () -> Unit, + onRunBatchDeleteSelected: () -> Unit, + onDismissActionResult: () -> Unit, + onSearchChanged: (String) -> Unit, + onSortChanged: (SortOption) -> Unit, + onViewModeToggle: () -> Unit, + onCheckForUpdates: () -> Unit, + onDismissUpdateCheckResult: () -> Unit, + onRefresh: () -> Unit, + onInstallClick: () -> Unit, + onOpenSettings: () -> Unit = {}, + onOpenUserManagement: () -> Unit = {}, + onOpenWelcomeScreen: () -> Unit = {}, + onOpenCustomConfig: (AppInfo) -> Unit = {} +) { + var showSearchBar by remember { mutableStateOf(false) } + var showFilterSheet by remember { mutableStateOf(false) } + var showOverflowMenu by remember { mutableStateOf(false) } + var showAboutSheet by remember { mutableStateOf(false) } + var selectedAppForActions by remember { mutableStateOf(null) } + var actionTargetApp by remember { mutableStateOf(null) } + var pendingAction by remember { mutableStateOf(null) } + var showBatchDeleteConfirm by remember { mutableStateOf(false) } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + floatingActionButton = { + FloatingActionButton(onClick = onInstallClick) { + Icon(Icons.Default.Add, contentDescription = stringResource(R.string.apps_list_cd_install)) + } + }, + topBar = { + if (showSearchBar) { + SearchBar( + query = searchQuery, + onQueryChange = onSearchChanged, + onClose = { + showSearchBar = false + onSearchChanged("") + } + ) + } else { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + navigationIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + title = { + if (selectionMode) { + Text(pluralStringResource(R.plurals.apps_list_n_selected, selectedAppIds.size, selectedAppIds.size)) + } else { + AppsListTitle(appVersion = appVersion) + } + }, + navigationIcon = { + if (selectionMode) { + IconButton(onClick = { onSetSelectionMode(false) }) { + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.apps_list_cd_exit_selection)) + } + } + }, + actions = { + if (selectionMode) { + IconButton(onClick = onSelectAllVisible) { + Icon(Icons.Default.Check, contentDescription = stringResource(R.string.apps_list_cd_select_all)) + } + IconButton( + enabled = selectedAppIds.isNotEmpty(), + onClick = { showBatchDeleteConfirm = true } + ) { + Icon(Icons.Default.Delete, contentDescription = stringResource(R.string.apps_list_cd_delete_selected)) + } + } else { + IconButton(onClick = { showSearchBar = true }) { + Icon(Icons.Default.Search, contentDescription = stringResource(R.string.apps_list_cd_search)) + } + IconButton(onClick = onOpenSettings) { + Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.settings_cd_open)) + } + IconButton(onClick = { showFilterSheet = true }) { + Icon(Icons.Default.FilterList, contentDescription = stringResource(R.string.filter_cd_open)) + } + AppsListOverflowMenu( + expanded = showOverflowMenu, + onExpandedChange = { showOverflowMenu = it }, + onRefresh = { + showOverflowMenu = false + onRefresh() + }, + onUserManagement = { + showOverflowMenu = false + onOpenUserManagement() + }, + onWelcomeScreen = { + showOverflowMenu = false + onOpenWelcomeScreen() + }, + onCheckForUpdates = { + showOverflowMenu = false + onCheckForUpdates() + }, + onAbout = { + showOverflowMenu = false + showAboutSheet = true + } + ) + } + } + ) + } + } + ) { padding -> + when { + loading && apps.isEmpty() -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + !initialized -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentAlignment = Alignment.Center + ) { + Text(stringResource(R.string.apps_list_init_failed)) + } + } + apps.isEmpty() -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentAlignment = Alignment.Center + ) { + when { + searchQuery.isNotEmpty() -> { + AppsEmptyState( + title = stringResource(R.string.apps_list_empty_search, searchQuery) + ) + } + !firmwareInstallState.hasRequiredComponents -> { + FirmwareMissingEmptyState( + firmwareInstallState = firmwareInstallState, + onInstallClick = onInstallClick + ) + } + else -> { + AppsEmptyState( + title = stringResource(R.string.apps_list_empty_no_apps), + actionLabel = stringResource(R.string.apps_list_install_app), + onAction = onInstallClick + ) + } + } + } + } + else -> { + when (viewMode) { + ViewMode.LIST -> AppsListView( + apps = apps, + onAppSelected = onAppSelected, + selectedAppIds = selectedAppIds, + selectionMode = selectionMode, + onSelectionClick = { onToggleAppSelection(it) }, + onActionLongPress = { + selectedAppForActions = it + onPrepareAppActions(it) + }, + modifier = Modifier.padding(padding) + ) + ViewMode.GRID -> AppsGridView( + apps = apps, + onAppSelected = onAppSelected, + selectedAppIds = selectedAppIds, + selectionMode = selectionMode, + onSelectionClick = { onToggleAppSelection(it) }, + onActionLongPress = { + selectedAppForActions = it + onPrepareAppActions(it) + }, + modifier = Modifier.padding(padding) + ) + } + } + } + } + + if (showFilterSheet) { + FilterSheet( + sortOption = sortOption, + viewMode = viewMode, + onSortChanged = onSortChanged, + onViewModeToggle = onViewModeToggle, + onDismiss = { showFilterSheet = false } + ) + } + + if (showAboutSheet) { + AboutSheet( + appVersion = appVersion, + onDismiss = { showAboutSheet = false } + ) + } + + selectedAppForActions?.let { app -> + AppActionsDialog( + app = app, + availableActions = resolveAvailableActions(app), + onDismiss = { selectedAppForActions = null }, + onActionSelected = { action -> + actionTargetApp = app + selectedAppForActions = null + pendingAction = action + }, + onShowInfo = { + selectedAppForActions = null + onShowAppInfo(app) + }, + onCustomConfig = { + selectedAppForActions = null + onOpenCustomConfig(app) + } + ) + } + + if (showBatchDeleteConfirm) { + AlertDialog( + onDismissRequest = { showBatchDeleteConfirm = false }, + title = { Text(pluralStringResource(R.plurals.batch_delete_title, selectedAppIds.size, selectedAppIds.size)) }, + text = { + ApplyDialogDim() + Text(pluralStringResource(R.plurals.batch_delete_message, selectedAppIds.size, selectedAppIds.size)) + }, + confirmButton = { + TextButton( + onClick = { + onRunBatchDeleteSelected() + showBatchDeleteConfirm = false + } + ) { + Text(stringResource(R.string.action_delete), color = MaterialTheme.colorScheme.error) + } + }, + dismissButton = { + TextButton(onClick = { showBatchDeleteConfirm = false }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } + + val action = pendingAction + if (action != null) { + val actionLabel = stringResource(action.labelResId) + AlertDialog( + onDismissRequest = { pendingAction = null }, + title = { + Text( + if (action.destructive) stringResource(R.string.confirm_delete_title, actionLabel) + else stringResource(R.string.confirm_reset_playtime_title) + ) + }, + text = { + ApplyDialogDim() + Text( + if (action.destructive) + stringResource(R.string.confirm_delete_message, actionLabel) + else + stringResource( + R.string.confirm_reset_playtime_message, + actionTargetApp?.title ?: stringResource(R.string.confirm_reset_playtime_this_app) + ) + ) + }, + confirmButton = { + TextButton( + onClick = { + val app = actionTargetApp + if (app != null) onRunAppAction(app, action) + pendingAction = null + actionTargetApp = null + } + ) { + Text( + if (action.destructive) stringResource(R.string.action_delete) else stringResource(R.string.action_yes), + color = if (action.destructive) MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.primary + ) + } + }, + dismissButton = { + TextButton(onClick = { + pendingAction = null + actionTargetApp = null + }) { + Text(if (action.destructive) stringResource(R.string.action_cancel) else stringResource(R.string.action_no)) + } + } + ) + } + + if (actionInProgress) { + AlertDialog( + onDismissRequest = {}, + title = { + Text( + if (pendingAction != null) + stringResource(R.string.action_deleting, stringResource(pendingAction!!.labelResId)) + else + stringResource(R.string.action_applying) + ) + }, + text = { + ApplyDialogDim() + Column { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + }, + confirmButton = {} + ) + } + + actionResultMessage?.let { message -> + AlertDialog( + onDismissRequest = onDismissActionResult, + title = { Text(stringResource(R.string.action_done)) }, + text = { + ApplyDialogDim() + Text(message) + }, + confirmButton = { + TextButton(onClick = onDismissActionResult) { + Text(stringResource(R.string.action_ok)) + } + } + ) + } + + updateCheckResult?.let { result -> + UpdateCheckDialog( + result = result, + onDismiss = onDismissUpdateCheckResult + ) + } + + if (updateCheckInProgress) { + AlertDialog( + onDismissRequest = {}, + title = { Text(stringResource(R.string.updates_check_title)) }, + text = { + ApplyDialogDim() + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.updates_check_progress)) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + }, + confirmButton = {} + ) + } +} + +private data class ParsedAppVersion( + val versionLabel: String, + val buildNumber: String? = null, + val revision: String? = null +) + +private const val ABOUT_DESCRIPTION_HTML = + """Vita3K is the world's first functional PS Vita and PS TV emulator, open source and written in C++ for Windows, Linux, macOS, and Android. Visit vita3k.org for more info, browse the project on GitHub if you want to contribute, or support us on Ko-fi.""" + +private const val ABOUT_CREDIT_HTML = + """Icon by Gordon Mackay.""" + +private const val ABOUT_FOOTER_HTML = + """Website | GitHub | Ko-fi | Discord""" + +@Composable +private fun AppsEmptyState( + title: String, + body: String? = null, + actionLabel: String? = null, + onAction: (() -> Unit)? = null +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .widthIn(max = 440.dp), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.92f) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + body?.takeIf { it.isNotBlank() }?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + if (actionLabel != null && onAction != null) { + FilledTonalButton( + onClick = onAction, + colors = ButtonDefaults.filledTonalButtonColors() + ) { + Text(actionLabel) + } + } + } + } +} + +@Composable +private fun FirmwareMissingEmptyState( + firmwareInstallState: FirmwareInstallState, + onInstallClick: () -> Unit +) { + val missingLabels = remember(firmwareInstallState) { + firmwareInstallState.components.missingRequiredComponentsInOrder.map(::firmwareComponentLabelRes) + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .widthIn(max = 440.dp), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.94f) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.apps_list_empty_firmware_title), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(R.string.apps_list_empty_firmware_body), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.72f) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 18.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = stringResource(R.string.apps_list_empty_firmware_missing_list_title), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + missingLabels.forEach { labelResId -> + Text( + text = "\u2022 ${stringResource(labelResId)}", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + } + } + } + FilledTonalButton(onClick = onInstallClick) { + Text(stringResource(R.string.apps_list_install_firmware)) + } + } + } +} + +private fun firmwareComponentLabelRes(component: FirmwareComponent): Int { + return when (component) { + FirmwareComponent.Preinstalled -> R.string.initial_setup_preinstall_title + FirmwareComponent.Main -> R.string.initial_setup_main_title + FirmwareComponent.FontPackage -> R.string.initial_setup_font_title + } +} + +private fun parseAppVersion(appVersion: String): ParsedAppVersion { + if (appVersion.isBlank()) { + return ParsedAppVersion(versionLabel = "") + } + + val parts = appVersion.split("-", limit = 3) + return ParsedAppVersion( + versionLabel = parts.getOrNull(0).orEmpty().ifBlank { appVersion }, + buildNumber = parts.getOrNull(1)?.takeIf { it.isNotBlank() }, + revision = parts.getOrNull(2)?.takeIf { it.isNotBlank() } + ) +} + +@Composable +private fun AppsListTitle(appVersion: String) { + val parsedVersion = remember(appVersion) { parseAppVersion(appVersion) } + val subtitle = remember(parsedVersion) { + when { + parsedVersion.versionLabel.isBlank() -> "" + parsedVersion.buildNumber.isNullOrBlank() -> parsedVersion.versionLabel + else -> "${parsedVersion.versionLabel} (${parsedVersion.buildNumber})" + } + } + + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = stringResource(R.string.apps_list_app_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (subtitle.isNotBlank()) { + Text( + text = subtitle, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +private fun AppsListOverflowMenu( + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + onRefresh: () -> Unit, + onUserManagement: () -> Unit, + onWelcomeScreen: () -> Unit, + onCheckForUpdates: () -> Unit, + onAbout: () -> Unit +) { + Box { + IconButton(onClick = { onExpandedChange(true) }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.apps_list_cd_more_options) + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { onExpandedChange(false) } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.apps_list_cd_refresh)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null + ) + }, + onClick = onRefresh + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.apps_list_menu_users)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null + ) + }, + onClick = onUserManagement + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.apps_list_menu_welcome)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null + ) + }, + onClick = onWelcomeScreen + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.apps_list_menu_check_updates)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.SystemUpdate, + contentDescription = null + ) + }, + onClick = onCheckForUpdates + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.apps_list_menu_about)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null + ) + }, + onClick = onAbout + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SearchBar( + query: String, + onQueryChange: (String) -> Unit, + onClose: () -> Unit +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboardController?.show() + } + + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + navigationIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + title = { + TextField( + value = query, + onValueChange = onQueryChange, + placeholder = { Text(stringResource(R.string.apps_list_search_placeholder)) }, + singleLine = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + ) + }, + navigationIcon = { + IconButton(onClick = { + keyboardController?.hide() + onClose() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.apps_list_cd_close_search)) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AboutSheet( + appVersion: String, + onDismiss: () -> Unit +) { + val parsedVersion = remember(appVersion) { parseAppVersion(appVersion) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + scrimColor = Color.Black.copy(alpha = SCRIM_ALPHA) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp, vertical = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Image( + painter = painterResource(id = R.mipmap.ic_launcher), + contentDescription = stringResource(R.string.apps_list_app_title), + modifier = Modifier + .size(132.dp) + .alpha(0.94f) + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text( + text = stringResource(R.string.apps_list_app_title), + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center + ) + if (parsedVersion.versionLabel.isNotBlank()) { + Text( + text = parsedVersion.versionLabel, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center + ) + } + parsedVersion.buildNumber?.let { build -> + Text( + text = stringResource(R.string.about_build, build), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + parsedVersion.revision?.let { revision -> + Text( + text = stringResource(R.string.about_revision, revision), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + HtmlText( + html = ABOUT_DESCRIPTION_HTML, + modifier = Modifier.fillMaxWidth(), + textStyle = MaterialTheme.typography.bodyLarge, + gravity = Gravity.CENTER + ) + HtmlText( + html = ABOUT_CREDIT_HTML, + modifier = Modifier.fillMaxWidth(), + textStyle = MaterialTheme.typography.bodySmall, + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + gravity = Gravity.CENTER + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = stringResource(R.string.about_legal_notice), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + HtmlText( + html = ABOUT_FOOTER_HTML, + modifier = Modifier.fillMaxWidth(), + textStyle = MaterialTheme.typography.titleMedium, + gravity = Gravity.CENTER + ) + + Spacer(modifier = Modifier.height(18.dp)) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun AppsListView( + apps: List, + onAppSelected: (AppInfo) -> Unit, + selectedAppIds: Set, + selectionMode: Boolean, + onSelectionClick: (AppInfo) -> Unit, + onActionLongPress: (AppInfo) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn(modifier = modifier.fillMaxSize()) { + items(apps, key = { it.titleId }) { app -> + val haptic = LocalHapticFeedback.current + ListItem( + headlineContent = { + Text(app.title, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + supportingContent = { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(app.titleId) + AppStatusBadges(app) + } + }, + leadingContent = { + AppIcon(app, size = 48) + }, + trailingContent = { + if (selectionMode && selectedAppIds.contains(app.titleId)) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(R.string.apps_list_cd_selected), + tint = MaterialTheme.colorScheme.primary + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.background, + headlineColor = MaterialTheme.colorScheme.onSurface, + supportingColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = Modifier.combinedClickable( + onClick = { + if (selectionMode) onSelectionClick(app) else onAppSelected(app) + }, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + if (selectionMode) onSelectionClick(app) else onActionLongPress(app) + } + ) + ) + } + } +} + +@Composable +private fun AppsGridView( + apps: List, + onAppSelected: (AppInfo) -> Unit, + selectedAppIds: Set, + selectionMode: Boolean, + onSelectionClick: (AppInfo) -> Unit, + onActionLongPress: (AppInfo) -> Unit, + modifier: Modifier = Modifier +) { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 120.dp), + contentPadding = PaddingValues(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.fillMaxSize() + ) { + items(apps, key = { it.titleId }) { app -> + AppGridItem( + app = app, + selected = selectedAppIds.contains(app.titleId), + selectionMode = selectionMode, + onClick = { + if (selectionMode) onSelectionClick(app) else onAppSelected(app) + }, + onLongClick = { + if (selectionMode) onSelectionClick(app) else onActionLongPress(app) + } + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun AppGridItem( + app: AppInfo, + selected: Boolean, + selectionMode: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit +) { + val haptic = LocalHapticFeedback.current + Card( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + onLongClick() + } + ), + shape = RoundedCornerShape(8.dp) + ) { + Column { + AppIcon(app, size = 120, modifier = Modifier.fillMaxWidth()) + Column(modifier = Modifier.padding(8.dp)) { + Text( + app.title, + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 4.dp) + ) { + AppStatusBadges(app) + if (selectionMode && selected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(R.string.apps_list_cd_selected), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } +} + +@Composable +private fun UpdateCheckDialog( + result: UpdateCheckResult, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val defaultDownloadUrl = "https://github.com/Vita3K/Vita3K/releases/tag/continuous" + val latestVersion = result.info.version.ifBlank { + if (result.info.buildNumber > 0L) "Build ${result.info.buildNumber}" else stringResource(R.string.updates_latest_build) + } + val publishedAt = remember(result.info.publishedAt) { formatPublishedAt(result.info.publishedAt) } + val downloadUrl = result.info.releaseUrl.ifBlank { defaultDownloadUrl } + + val titleRes = when (result.status) { + UpdateCheckStatus.UpdateAvailable -> R.string.updates_available_title + UpdateCheckStatus.CustomBuildCanUpdate -> R.string.updates_official_available_title + else -> R.string.updates_check_title + } + + val summary = remember(result, latestVersion, publishedAt) { + when (result.status) { + UpdateCheckStatus.UpdateAvailable -> + if (publishedAt.isBlank()) { + context.getString( + R.string.updates_summary_available, + result.currentDisplayVersion.ifBlank { "Unknown" }, + latestVersion + ) + } else { + context.getString( + R.string.updates_summary_available_with_date, + result.currentDisplayVersion.ifBlank { "Unknown" }, + latestVersion, + publishedAt + ) + } + UpdateCheckStatus.CustomBuildCanUpdate -> + if (publishedAt.isBlank()) { + context.getString(R.string.updates_summary_custom_build, latestVersion) + } else { + context.getString(R.string.updates_summary_custom_build_with_date, latestVersion, publishedAt) + } + else -> result.message + } + } + + val showDownloadAction = result.status == UpdateCheckStatus.UpdateAvailable || + result.status == UpdateCheckStatus.CustomBuildCanUpdate + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(titleRes)) }, + text = { + ApplyDialogDim() + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 320.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text(summary) + if (result.info.notes.isNotBlank()) { + Text( + text = result.info.notes, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, + confirmButton = { + if (showDownloadAction) { + TextButton(onClick = { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(downloadUrl))) + onDismiss() + }) { + Text(stringResource(R.string.updates_open_download_page)) + } + } else { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_ok)) + } + } + }, + dismissButton = { + if (showDownloadAction) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_cancel)) + } + } + } + ) +} + +private fun formatPublishedAt(publishedAt: String): String { + if (publishedAt.isBlank()) { + return "" + } + + return runCatching { + val instant = java.time.Instant.parse(publishedAt) + java.time.format.DateTimeFormatter + .ofLocalizedDateTime(java.time.format.FormatStyle.SHORT) + .withZone(java.time.ZoneId.systemDefault()) + .format(instant) + }.getOrElse { publishedAt } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun AppActionsDialog( + app: AppInfo, + availableActions: Set, + onDismiss: () -> Unit, + onActionSelected: (AppAction) -> Unit, + onShowInfo: () -> Unit, + onCustomConfig: () -> Unit = {} +) { + var showDeleteSubmenu by remember { mutableStateOf(false) } + + if (showDeleteSubmenu) { + val availableDeletes = AppAction.entries + .filter { it.group == AppActionGroup.DELETE && availableActions.contains(it) } + + AppMenuDialog( + title = app.title, + onDismiss = { showDeleteSubmenu = false } + ) { + availableDeletes.forEach { action -> + AppMenuRow( + label = stringResource(action.labelResId), + labelColor = MaterialTheme.colorScheme.error, + onClick = { + showDeleteSubmenu = false + onActionSelected(action) + } + ) + } + AppMenuRow( + label = stringResource(R.string.action_back), + labelColor = MaterialTheme.colorScheme.onSurfaceVariant, + icon = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp) + ) + }, + onClick = { showDeleteSubmenu = false } + ) + } + return + } + + val hasAnyDelete = AppAction.entries + .any { it.group == AppActionGroup.DELETE && availableActions.contains(it) } + val otherActions = AppAction.entries.filter { it.group == AppActionGroup.OTHER } + + AppMenuDialog(title = app.title, onDismiss = onDismiss) { + // Information + AppMenuRow( + label = stringResource(R.string.app_menu_information), + icon = { + Icon( + Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp) + ) + }, + onClick = { onDismiss(); onShowInfo() } + ) + // Custom Config + AppMenuRow( + label = stringResource(R.string.app_menu_custom_config), + icon = { + Icon( + Icons.Default.Settings, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp) + ) + }, + onClick = { onCustomConfig() } + ) + + // Other actions (reset last played, etc.) + otherActions.forEach { action -> + val enabled = availableActions.contains(action) + val actionIcon: (@Composable () -> Unit)? = when (action) { + AppAction.RESET_LAST_PLAYED -> ({ + Icon( + Icons.Default.History, + contentDescription = null, + tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + modifier = Modifier.size(18.dp) + ) + }) + else -> null + } + AppMenuRow( + label = stringResource(action.labelResId), + labelColor = if (enabled) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + icon = actionIcon, + onClick = { if (enabled) { onActionSelected(action) } } + ) + } + + + + // Delete — drills into submenu + if (hasAnyDelete) { + AppMenuRow( + label = stringResource(R.string.app_menu_delete), + labelColor = MaterialTheme.colorScheme.error, + icon = { + Icon( + Icons.Default.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp) + ) + }, + onClick = { showDeleteSubmenu = true } + ) + } + } +} + +@Composable +private fun AppMenuDialog( + title: String, + onDismiss: () -> Unit, + content: @Composable ColumnScope.() -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + ApplyDialogDim() + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 0.dp, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Column(modifier = Modifier.padding(vertical = 8.dp)) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp) + ) + content() + Spacer(modifier = Modifier.height(4.dp)) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun AppMenuRow( + label: String, + labelColor: Color = MaterialTheme.colorScheme.onSurface, + icon: (@Composable () -> Unit)? = null, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable(onClick = onClick) + .padding(horizontal = 20.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + if (icon != null) { + Box(modifier = Modifier.size(18.dp)) { icon() } + } else { + Spacer(modifier = Modifier.size(18.dp)) + } + Text(label, style = MaterialTheme.typography.bodyLarge, color = labelColor) + } +} + +@Composable +internal fun AppIcon(app: AppInfo, size: Int, modifier: Modifier = Modifier) { + val iconFile = app.iconFile + if (iconFile != null) { + AsyncImage( + model = iconFile, + contentDescription = app.title, + contentScale = ContentScale.Crop, + modifier = modifier + .size(size.dp) + .clip(RoundedCornerShape(4.dp)) + ) + } else { + Box( + modifier = modifier + .size(size.dp) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + Text( + app.title.take(2).uppercase(), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +internal fun CompatBadge(app: AppInfo) { + val compat = app.compatibility + Surface( + color = Color(compat.colorHex), + shape = RoundedCornerShape(4.dp) + ) { + Text( + stringResource(compat.labelResId), + style = MaterialTheme.typography.labelSmall, + color = Color(compat.onColorHex), + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } +} + +@Composable +private fun AppStatusBadges(app: AppInfo) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CompatBadge(app) + if (app.hasCustomConfig) { + CustomConfigBadge() + } + } +} + +@Composable +private fun CustomConfigBadge() { + Surface( + color = Color(0xFF2458A6), + contentColor = Color(0xFFF6FAFF), + shape = RoundedCornerShape(999.dp) + ) { + Text( + text = stringResource(R.string.apps_list_custom_config_badge), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp) + ) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/FilterSheet.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/FilterSheet.kt new file mode 100644 index 000000000..4c863913b --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/FilterSheet.kt @@ -0,0 +1,116 @@ +package org.vita3k.emulator.ui.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.GridView +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R +import org.vita3k.emulator.data.SortOption +import org.vita3k.emulator.data.ViewMode +import org.vita3k.emulator.ui.theme.SCRIM_ALPHA + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterSheet( + sortOption: SortOption, + viewMode: ViewMode, + onSortChanged: (SortOption) -> Unit, + onViewModeToggle: () -> Unit, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + scrimColor = Color.Black.copy(alpha = SCRIM_ALPHA) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 32.dp) + ) { + Text( + stringResource(R.string.filter_title), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 16.dp) + ) + + // Sort section + Text( + stringResource(R.string.filter_section_sort), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(bottom = 8.dp) + ) + SortOption.entries.forEach { option -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = option == sortOption, + onClick = { onSortChanged(option) } + ) + Text( + text = when (option) { + SortOption.TITLE -> stringResource(R.string.apps_list_sort_title) + SortOption.LAST_PLAYED -> stringResource(R.string.apps_list_sort_last_played) + SortOption.COMPATIBILITY -> stringResource(R.string.apps_list_sort_compatibility) + }, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .weight(1f) + .padding(start = 8.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // View mode section + Text( + stringResource(R.string.filter_section_view), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(bottom = 8.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + FilterChip( + selected = viewMode == ViewMode.LIST, + onClick = { if (viewMode != ViewMode.LIST) onViewModeToggle() }, + label = { Text(stringResource(R.string.filter_view_list)) }, + leadingIcon = { + Icon( + Icons.AutoMirrored.Filled.List, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + FilterChip( + selected = viewMode == ViewMode.GRID, + onClick = { if (viewMode != ViewMode.GRID) onViewModeToggle() }, + label = { Text(stringResource(R.string.filter_view_grid)) }, + leadingIcon = { + Icon( + Icons.Default.GridView, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + } + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/InitialSetupScreen.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/InitialSetupScreen.kt new file mode 100644 index 000000000..244a9f733 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/InitialSetupScreen.kt @@ -0,0 +1,528 @@ +package org.vita3k.emulator.ui.screens + +import android.view.Gravity +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R +import org.vita3k.emulator.data.FirmwareInstallState +import org.vita3k.emulator.data.FirmwareLinks +import org.vita3k.emulator.ui.components.HtmlText + +private const val INITIAL_SETUP_INFO_HTML = + """
To get started, please install all PS Vita firmware files.

A comprehensive guide is available on the Quickstart page.
Check the commercial and homebrew compatibility lists to see what currently runs.

Contributions are welcome on GitHub, and additional help is available on Discord.
""" + +private val setupPanelColor = Color(0xFF1F1D1C) +private val setupCardColor = Color(0xFF2B2B2B) +private val setupTextColor = Color(0xFFF7F3EC) + +@Composable +fun InitialSetupScreen( + firmwareInstallState: FirmwareInstallState, + preferredLanguageIndex: Int, + onInstallFirmware: () -> Unit, + dismissLabel: String, + onSkip: () -> Unit, + onFinish: () -> Unit +) { + val systemBars = WindowInsets.systemBars.asPaddingValues() + var page by rememberSaveable { mutableIntStateOf(0) } + var firmwareLocaleIndex by rememberSaveable { + mutableIntStateOf(FirmwareLinks.coerceLocaleIndex(preferredLanguageIndex)) + } + + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf( + MaterialTheme.colorScheme.primary.copy(alpha = 0.16f), + MaterialTheme.colorScheme.background, + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.38f) + ) + ) + ) + ) { + BackgroundOrb( + modifier = Modifier + .align(Alignment.TopStart) + .padding(start = 24.dp, top = 88.dp), + size = 180.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.09f) + ) + BackgroundOrb( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 8.dp, bottom = 72.dp), + size = 220.dp, + color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.08f) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = 20.dp, + end = 20.dp, + top = systemBars.calculateTopPadding() + 8.dp, + bottom = systemBars.calculateBottomPadding() + 20.dp + ) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + if (page > 0) { + TextButton(onClick = { page-- }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + Spacer(modifier = Modifier.width(6.dp)) + Text(stringResource(R.string.initial_setup_back)) + } + } else { + Spacer(modifier = Modifier.width(96.dp)) + } + Spacer(modifier = Modifier.weight(1f)) + TextButton(onClick = onSkip) { + Text(dismissLabel) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Surface( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + shape = RoundedCornerShape(32.dp), + color = setupPanelColor, + contentColor = setupTextColor, + tonalElevation = 6.dp, + border = BorderStroke( + 1.dp, + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f) + ) + ) { + Crossfade( + targetState = page, + animationSpec = tween(durationMillis = 220), + label = "initial-setup-page" + ) { currentPage -> + when (currentPage) { + 0 -> WelcomePage() + else -> FirmwareSetupPage( + firmwareInstallState = firmwareInstallState, + firmwareLocaleIndex = firmwareLocaleIndex, + onFirmwareLocaleSelected = { firmwareLocaleIndex = it }, + onInstallFirmware = onInstallFirmware + ) + } + } + } + + Spacer(modifier = Modifier.height(18.dp)) + + SetupPageIndicator( + currentPage = page, + totalPages = 2, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(1.dp)) + if (page == 0) { + Button(onClick = { page = 1 }) { + Text(stringResource(R.string.initial_setup_next)) + Spacer(modifier = Modifier.width(8.dp)) + Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null) + } + } else { + Button(onClick = onFinish) { + Text(stringResource(R.string.initial_setup_open_library)) + } + } + } + } + } +} + +@Composable +private fun BackgroundOrb( + modifier: Modifier = Modifier, + size: androidx.compose.ui.unit.Dp, + color: Color +) { + Box( + modifier = modifier + .size(size) + .clip(CircleShape) + .background(color) + ) +} + +@Composable +private fun WelcomePage() { + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 28.dp, vertical = 32.dp) + .heightIn(min = maxHeight - 64.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.mipmap.ic_launcher), + contentDescription = stringResource(R.string.apps_list_app_title), + modifier = Modifier + .size(112.dp) + .alpha(0.96f) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = stringResource(R.string.initial_setup_welcome_title), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + color = setupTextColor, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = stringResource(R.string.initial_setup_welcome_body), + style = MaterialTheme.typography.bodyLarge, + color = setupTextColor, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(18.dp)) + + HtmlText( + html = INITIAL_SETUP_INFO_HTML, + modifier = Modifier.fillMaxWidth(), + textStyle = MaterialTheme.typography.bodyLarge, + textColor = setupTextColor, + gravity = Gravity.CENTER + ) + + Spacer(modifier = Modifier.height(18.dp)) + + Text( + text = stringResource(R.string.initial_setup_piracy_notice), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun FirmwareSetupPage( + firmwareInstallState: FirmwareInstallState, + firmwareLocaleIndex: Int, + onFirmwareLocaleSelected: (Int) -> Unit, + onInstallFirmware: () -> Unit +) { + val uriHandler = LocalUriHandler.current + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 20.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Text( + text = stringResource(R.string.initial_setup_firmware_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + color = setupTextColor + ) + Text( + text = stringResource(R.string.initial_setup_firmware_body), + style = MaterialTheme.typography.bodyLarge, + color = setupTextColor + ) + + FirmwareCard( + title = stringResource(R.string.initial_setup_preinstall_title), + installed = firmwareInstallState.components.preinstalled, + missingStatusText = stringResource(R.string.initial_setup_status_optional) + ) { + if (!firmwareInstallState.components.preinstalled) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + FilledTonalButton(onClick = { uriHandler.openUri(FirmwareLinks.PREINSTALL_URL) }) { + Text(stringResource(R.string.initial_setup_download)) + } + Button(onClick = onInstallFirmware) { + Text(stringResource(R.string.initial_setup_install_pup)) + } + } + } + } + + FirmwareCard( + title = stringResource(R.string.initial_setup_main_title), + installed = firmwareInstallState.components.main + ) { + if (!firmwareInstallState.components.main) { + FirmwareLanguagePicker( + selectedIndex = firmwareLocaleIndex, + onSelected = onFirmwareLocaleSelected + ) + Spacer(modifier = Modifier.height(12.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + FilledTonalButton(onClick = { + uriHandler.openUri(FirmwareLinks.firmwareDownloadUrl(firmwareLocaleIndex)) + }) { + Text(stringResource(R.string.initial_setup_download)) + } + Button(onClick = onInstallFirmware) { + Text(stringResource(R.string.initial_setup_install_pup)) + } + } + } + } + + FirmwareCard( + title = stringResource(R.string.initial_setup_font_title), + installed = firmwareInstallState.components.fontPackage + ) { + if (!firmwareInstallState.components.fontPackage) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + FilledTonalButton(onClick = { uriHandler.openUri(FirmwareLinks.FONT_PACKAGE_URL) }) { + Text(stringResource(R.string.initial_setup_download)) + } + Button(onClick = onInstallFirmware) { + Text(stringResource(R.string.initial_setup_install_pup)) + } + } + } + } + } +} + +@Composable +private fun FirmwareLanguagePicker( + selectedIndex: Int, + onSelected: (Int) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringResource(R.string.initial_setup_select_firmware_language), + style = MaterialTheme.typography.labelLarge, + color = setupTextColor + ) + Box { + OutlinedButton(onClick = { expanded = true }) { + Text(stringResource(FirmwareLinks.locales[FirmwareLinks.coerceLocaleIndex(selectedIndex)].nameResId)) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + FirmwareLinks.locales.forEachIndexed { index, locale -> + DropdownMenuItem( + text = { Text(stringResource(locale.nameResId)) }, + onClick = { + onSelected(index) + expanded = false + } + ) + } + } + } + } +} + +@Composable +private fun FirmwareCard( + title: String, + installed: Boolean, + missingStatusText: String? = null, + content: @Composable ColumnScope.() -> Unit +) { + val resolvedMissingStatusText = missingStatusText ?: stringResource(R.string.initial_setup_status_missing) + + Surface( + shape = RoundedCornerShape(28.dp), + color = setupCardColor, + contentColor = setupTextColor, + border = BorderStroke( + 1.dp, + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f) + ) + ) { + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = setupTextColor, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(12.dp)) + StatusBadge( + installed = installed, + missingStatusText = resolvedMissingStatusText + ) + } + content() + } + } +} + +@Composable +private fun StatusBadge(installed: Boolean, missingStatusText: String) { + val color = if (installed) Color(0xFF1B8A5A) else MaterialTheme.colorScheme.tertiary + + Surface( + shape = CircleShape, + color = color.copy(alpha = 0.13f) + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(6.dp) + .clip(CircleShape) + .background(color) + ) + Text( + text = if (installed) { + stringResource(R.string.initial_setup_status_installed) + } else { + missingStatusText + }, + style = MaterialTheme.typography.labelLarge, + color = color, + fontWeight = FontWeight.SemiBold + ) + } + } +} + +@Composable +private fun SetupPageIndicator( + currentPage: Int, + totalPages: Int, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(totalPages) { page -> + val width by animateDpAsState( + targetValue = if (page == currentPage) 22.dp else 8.dp, + animationSpec = tween(durationMillis = 220), + label = "initial-setup-indicator" + ) + Box( + modifier = Modifier + .padding(horizontal = 4.dp) + .height(8.dp) + .width(width) + .clip(CircleShape) + .background( + if (page == currentPage) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f) + ) + ) + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/InstallSheet.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/InstallSheet.kt new file mode 100644 index 000000000..e8b0a41aa --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/InstallSheet.kt @@ -0,0 +1,273 @@ +package org.vita3k.emulator.ui.screens + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FolderZip +import androidx.compose.material.icons.filled.Inventory2 +import androidx.compose.material.icons.filled.Key +import androidx.compose.material.icons.filled.SystemUpdate +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R +import org.vita3k.emulator.ui.viewmodel.DeleteSourceOption +import org.vita3k.emulator.ui.viewmodel.InstallResult +import org.vita3k.emulator.ui.viewmodel.InstallResultStatus +import org.vita3k.emulator.ui.viewmodel.InstallType + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun InstallBottomSheet( + onDismiss: () -> Unit, + onSelectType: (InstallType) -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val itemColors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface, + headlineColor = MaterialTheme.colorScheme.onSurface, + supportingColor = MaterialTheme.colorScheme.onSurfaceVariant, + leadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + Text( + stringResource(R.string.install_sheet_title), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) + ) + ListItem( + headlineContent = { Text(stringResource(R.string.install_firmware_title)) }, + supportingContent = { Text(stringResource(R.string.install_firmware_desc)) }, + leadingContent = { Icon(Icons.Default.SystemUpdate, contentDescription = null) }, + colors = itemColors, + modifier = Modifier.clickable { onSelectType(InstallType.FIRMWARE) } + ) + ListItem( + headlineContent = { Text(stringResource(R.string.install_pkg_title)) }, + supportingContent = { Text(stringResource(R.string.install_pkg_desc)) }, + leadingContent = { Icon(Icons.Default.Inventory2, contentDescription = null) }, + colors = itemColors, + modifier = Modifier.clickable { onSelectType(InstallType.PKG) } + ) + ListItem( + headlineContent = { Text(stringResource(R.string.install_archive_title)) }, + supportingContent = { Text(stringResource(R.string.install_archive_desc)) }, + leadingContent = { Icon(Icons.Default.FolderZip, contentDescription = null) }, + colors = itemColors, + modifier = Modifier.clickable { onSelectType(InstallType.ARCHIVE) } + ) + ListItem( + headlineContent = { Text(stringResource(R.string.install_license_title)) }, + supportingContent = { Text(stringResource(R.string.install_license_desc)) }, + leadingContent = { Icon(Icons.Default.Key, contentDescription = null) }, + colors = itemColors, + modifier = Modifier.clickable { onSelectType(InstallType.LICENSE) } + ) + Spacer(modifier = Modifier.height(24.dp)) + } +} + +@Composable +fun InstallProgressDialog( + progress: Int, + statusMessage: String +) { + AlertDialog( + onDismissRequest = {}, + title = { Text(stringResource(R.string.install_progress_title)) }, + text = { + Column { + Text(statusMessage) + Spacer(modifier = Modifier.height(16.dp)) + if (progress > 0) { + LinearProgressIndicator( + progress = { progress / 100f }, + modifier = Modifier.fillMaxWidth(), + drawStopIndicator = {} + ) + Text( + "$progress%", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 4.dp) + ) + } else { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + } + }, + confirmButton = {} + ) +} + +@Composable +fun InstallResultDialog( + result: InstallResult, + onConfirm: (List) -> Unit +) { + var selectedDeleteOptions by remember(result) { + mutableStateOf(emptySet()) + } + + AlertDialog( + onDismissRequest = { onConfirm(emptyList()) }, + title = { + Text( + when (result.status) { + InstallResultStatus.SUCCESS -> stringResource(R.string.install_result_success_title) + InstallResultStatus.PARTIAL -> stringResource(R.string.install_result_partial_title) + InstallResultStatus.ERROR -> stringResource(R.string.install_result_failed_title) + } + ) + }, + text = { + Column { + Text(result.message) + if (result.deleteOptions.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + result.deleteOptions.forEachIndexed { index, option -> + Row( + modifier = Modifier.fillMaxWidth() + ) { + Checkbox( + checked = index in selectedDeleteOptions, + onCheckedChange = { checked -> + selectedDeleteOptions = if (checked) { + selectedDeleteOptions + index + } else { + selectedDeleteOptions - index + } + } + ) + Text( + text = option.label, + modifier = Modifier.padding(top = 12.dp) + ) + } + } + } + } + }, + confirmButton = { + TextButton( + onClick = { + onConfirm( + selectedDeleteOptions + .sorted() + .map(result.deleteOptions::get) + ) + } + ) { + Text(stringResource(R.string.action_ok)) + } + } + ) +} + +@Composable +fun LicenseSourceDialog( + title: String, + message: String, + onSelectLicenseFile: () -> Unit, + onEnterZrif: (String) -> Unit, + onDismiss: () -> Unit +) { + var showManualEntry by remember { mutableStateOf(false) } + var zrif by remember { mutableStateOf("") } + + if (showManualEntry) { + AlertDialog( + onDismissRequest = { showManualEntry = false }, + title = { Text(stringResource(R.string.zrif_dialog_title)) }, + text = { + OutlinedTextField( + value = zrif, + onValueChange = { zrif = it.trim() }, + label = { Text(stringResource(R.string.zrif_dialog_hint)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + }, + confirmButton = { + TextButton( + onClick = { onEnterZrif(zrif) }, + enabled = zrif.isNotEmpty() + ) { Text(stringResource(R.string.action_install)) } + }, + dismissButton = { + TextButton(onClick = { showManualEntry = false }) { Text(stringResource(R.string.action_back)) } + } + ) + } else { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column { + Text( + message, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(16.dp)) + FilledTonalButton( + onClick = onSelectLicenseFile, + modifier = Modifier.fillMaxWidth() + ) { Text(stringResource(R.string.license_select_file)) } + Spacer(modifier = Modifier.height(8.dp)) + FilledTonalButton( + onClick = { showManualEntry = true }, + modifier = Modifier.fillMaxWidth() + ) { Text(stringResource(R.string.license_enter_zrif)) } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } + } + ) + } +} + +@Composable +fun ArchiveInstallSourceDialog( + onSelectFile: () -> Unit, + onSelectFolder: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.install_archive_source_title)) }, + text = { + Column { + Text( + stringResource(R.string.install_archive_source_message), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(16.dp)) + FilledTonalButton( + onClick = onSelectFile, + modifier = Modifier.fillMaxWidth() + ) { Text(stringResource(R.string.install_archive_source_file)) } + Spacer(modifier = Modifier.height(8.dp)) + FilledTonalButton( + onClick = onSelectFolder, + modifier = Modifier.fillMaxWidth() + ) { Text(stringResource(R.string.install_archive_source_folder)) } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } + } + ) +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/UserManagementScreen.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/UserManagementScreen.kt new file mode 100644 index 000000000..1186814ac --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/UserManagementScreen.kt @@ -0,0 +1,418 @@ +package org.vita3k.emulator.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AssistChip +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R +import org.vita3k.emulator.data.NativeUser +import org.vita3k.emulator.ui.theme.ApplyDialogDim +import org.vita3k.emulator.ui.viewmodel.UserManagementViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UserManagementScreen( + viewModel: UserManagementViewModel, + onBack: () -> Unit, + onUsersChanged: () -> Unit +) { + var showCreateDialog by rememberSaveable { mutableStateOf(false) } + var pendingDeletion by remember { mutableStateOf(null) } + val activeUser = viewModel.users.firstOrNull { it.active } + + LaunchedEffect(Unit) { + viewModel.load() + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + navigationIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + title = { Text(stringResource(R.string.user_management_title)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.action_back) + ) + } + }, + actions = { + IconButton(onClick = viewModel::load, enabled = !viewModel.loading && !viewModel.busy) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(R.string.user_management_refresh) + ) + } + } + ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text(stringResource(R.string.user_management_create_user)) }, + icon = { Icon(Icons.Default.Add, contentDescription = null) }, + onClick = { showCreateDialog = true }, + expanded = true + ) + } + ) { padding -> + when { + viewModel.loading && viewModel.users.isEmpty() -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + else -> { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentPadding = PaddingValues(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 96.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + item { + ActiveUserHero( + activeUser = activeUser, + onCreateUser = { showCreateDialog = true } + ) + } + + if (viewModel.users.isEmpty()) { + item { + EmptyUsersCard(onCreateUser = { showCreateDialog = true }) + } + } else { + items(viewModel.users, key = { it.id }) { user -> + UserCard( + user = user, + busy = viewModel.busy, + onActivate = { viewModel.activateUser(user, onUsersChanged) }, + onDelete = { pendingDeletion = user } + ) + } + } + } + } + } + } + + if (showCreateDialog) { + CreateUserDialog( + busy = viewModel.busy, + onDismiss = { showCreateDialog = false }, + onCreate = { name -> + showCreateDialog = false + viewModel.createUser(name, onUsersChanged) + } + ) + } + + pendingDeletion?.let { user -> + AlertDialog( + onDismissRequest = { pendingDeletion = null }, + title = { Text(stringResource(R.string.user_management_delete_title)) }, + text = { + ApplyDialogDim() + Text(stringResource(R.string.user_management_delete_message, user.name)) + }, + confirmButton = { + TextButton( + onClick = { + pendingDeletion = null + viewModel.deleteUser(user, onUsersChanged) + } + ) { + Text( + text = stringResource(R.string.action_delete), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { pendingDeletion = null }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } + + viewModel.operationResult?.let { result -> + AlertDialog( + onDismissRequest = { viewModel.dismissOperationResult() }, + title = { + Text( + if (result.isError) { + stringResource(R.string.settings_opt_error) + } else { + stringResource(R.string.settings_success_title) + } + ) + }, + text = { + ApplyDialogDim() + Text(result.message) + }, + confirmButton = { + TextButton(onClick = { viewModel.dismissOperationResult() }) { + Text(stringResource(R.string.action_ok)) + } + } + ) + } +} + +@Composable +private fun ActiveUserHero( + activeUser: NativeUser?, + onCreateUser: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f) + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.1f), + shape = RoundedCornerShape(20.dp) + ) { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(12.dp) + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.user_management_active_user), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.72f) + ) + Text( + text = activeUser?.name ?: stringResource(R.string.user_management_active_none), + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.onPrimaryContainer, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + activeUser?.let { + Text( + text = it.id, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.78f) + ) + } + } + } + FilledTonalButton(onClick = onCreateUser) { + Icon(Icons.Default.Add, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text(stringResource(R.string.user_management_create_user)) + } + } + } +} + +@Composable +private fun EmptyUsersCard( + onCreateUser: () -> Unit +) { + ElevatedCard(shape = RoundedCornerShape(24.dp)) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = stringResource(R.string.user_management_empty_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(R.string.user_management_empty_message), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + FilledTonalButton(onClick = onCreateUser) { + Text(stringResource(R.string.user_management_create_user)) + } + } + } +} + +@Composable +private fun UserCard( + user: NativeUser, + busy: Boolean, + onActivate: () -> Unit, + onDelete: () -> Unit +) { + ElevatedCard(shape = RoundedCornerShape(24.dp)) { + Column( + modifier = Modifier.padding(18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = user.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = user.id, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (user.active) { + AssistChip( + onClick = {}, + enabled = false, + label = { Text(stringResource(R.string.user_management_active_badge)) } + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + FilledTonalButton( + onClick = onActivate, + enabled = !busy && !user.active, + modifier = Modifier.weight(1f) + ) { + Text( + text = if (user.active) { + stringResource(R.string.user_management_active_badge) + } else { + stringResource(R.string.user_management_switch_user) + } + ) + } + OutlinedButton( + onClick = onDelete, + enabled = !busy, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Delete, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text(stringResource(R.string.user_management_delete_user)) + } + } + } + } +} + +@Composable +private fun CreateUserDialog( + busy: Boolean, + onDismiss: () -> Unit, + onCreate: (String) -> Unit +) { + var userName by rememberSaveable { mutableStateOf("Vita3K") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.user_management_create_user)) }, + text = { + ApplyDialogDim() + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.user_management_create_prompt)) + OutlinedTextField( + value = userName, + onValueChange = { userName = it }, + singleLine = true, + label = { Text(stringResource(R.string.user_management_name_label)) }, + modifier = Modifier.fillMaxWidth() + ) + } + }, + confirmButton = { + TextButton( + enabled = !busy && userName.trim().isNotEmpty(), + onClick = { onCreate(userName.trim()) } + ) { + Text(stringResource(R.string.action_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_cancel)) + } + } + ) +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/EmulationPauseMenu.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/EmulationPauseMenu.kt new file mode 100644 index 000000000..bade35c27 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/EmulationPauseMenu.kt @@ -0,0 +1,1015 @@ +package org.vita3k.emulator.ui.screens.emulation + +import android.view.View +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Done +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Restore +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.SportsEsports +import androidx.compose.material.icons.filled.TouchApp +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.Emulator +import org.vita3k.emulator.R +import org.vita3k.emulator.data.RestartRequiredSetting +import org.vita3k.emulator.ui.components.OverlayEditorPalette +import org.vita3k.emulator.ui.rememberConnectedGamepads +import org.vita3k.emulator.ui.screens.settings.ControllerMappingSections +import org.vita3k.emulator.ui.screens.settings.SettingsCategory +import org.vita3k.emulator.ui.screens.settings.SettingsCategoryBody +import org.vita3k.emulator.ui.screens.settings.SettingsCategoryStrip +import org.vita3k.emulator.ui.screens.settings.SettingsHelpEntry +import org.vita3k.emulator.ui.screens.settings.SettingsHelpSheet +import org.vita3k.emulator.ui.screens.settings.SettingsLoadingState +import org.vita3k.emulator.ui.screens.settings.SettingsNote +import org.vita3k.emulator.ui.screens.settings.OverlaySettingsRows +import org.vita3k.emulator.ui.screens.settings.settingsFilterChipColors +import org.vita3k.emulator.ui.screens.settings.settingsCategories +import org.vita3k.emulator.ui.theme.ApplyDialogDim +import org.vita3k.emulator.ui.theme.Vita3KTheme +import org.vita3k.emulator.ui.viewmodel.EmulationSessionViewModel +import org.vita3k.emulator.ui.viewmodel.SettingsViewModel + +private enum class PauseSettingsScope { + Global, + CustomConfig +} + +object EmulationPauseMenuHost { + @JvmStatic + fun attach( + activity: Emulator, + composeView: ComposeView, + sessionViewModel: EmulationSessionViewModel, + settingsViewModel: SettingsViewModel, + globalSettingsViewModel: SettingsViewModel + ) { + composeView.setContent { + Vita3KTheme { + EmulationPauseMenu( + activity = activity, + sessionViewModel = sessionViewModel, + settingsViewModel = settingsViewModel, + globalSettingsViewModel = globalSettingsViewModel, + hostView = composeView + ) + } + } + } +} + +private enum class PauseMenuTab(val labelRes: Int, val icon: ImageVector) { + Session(R.string.emulation_tab_session, Icons.Default.Pause), + Controls(R.string.emulation_tab_controls, Icons.Default.TouchApp), + Controller(R.string.emulation_tab_controller, Icons.Default.SportsEsports), + Settings(R.string.emulation_tab_settings, Icons.Default.Settings) +} + +private fun formatRestartRequiredSettings( + context: android.content.Context, + settings: List +): String = settings.joinToString(separator = "\n- ", prefix = "- ") { setting -> + context.getString(setting.labelResId) +} + +@Composable +private fun EmulationPauseMenu( + activity: Emulator, + sessionViewModel: EmulationSessionViewModel, + settingsViewModel: SettingsViewModel, + globalSettingsViewModel: SettingsViewModel, + hostView: ComposeView +) { + val uiState = sessionViewModel.uiState + val context = LocalContext.current + var selectedTab by remember { mutableStateOf(PauseMenuTab.Session) } + var pendingHelp by remember { mutableStateOf(null) } + var pendingRestartSettings by remember { mutableStateOf?>(null) } + var requestedSettingsScope by remember { mutableStateOf(null) } + val connectedGamepads = rememberConnectedGamepads(context) + val menuVisibilityState = remember { MutableTransitionState(false) } + val controlsVisibilityState = remember { MutableTransitionState(false) } + + LaunchedEffect(uiState.titleId) { + if (uiState.titleId.isNotBlank()) { + settingsViewModel.load(uiState.titleId) + } + } + + LaunchedEffect(uiState.showMenu) { + if (uiState.showMenu && !globalSettingsViewModel.isLoaded(titleId = null)) { + globalSettingsViewModel.load(titleId = null) + } + } + + LaunchedEffect(uiState.showMenu) { + if (uiState.showMenu) { + selectedTab = PauseMenuTab.Session + } + menuVisibilityState.targetState = uiState.showMenu + } + + LaunchedEffect(uiState.isEditingControls) { + controlsVisibilityState.targetState = uiState.isEditingControls + } + + val hostVisible = menuVisibilityState.currentState || + menuVisibilityState.targetState || + controlsVisibilityState.currentState || + controlsVisibilityState.targetState || + uiState.showExitConfirmation || + settingsViewModel.operationResult != null || + globalSettingsViewModel.operationResult != null || + pendingRestartSettings != null || + pendingHelp != null + + SideEffect { + hostView.visibility = if (hostVisible) View.VISIBLE else View.GONE + } + + Box(modifier = Modifier.fillMaxSize()) { + ControlsEditorBar( + visibleState = controlsVisibilityState, + onDone = { sessionViewModel.finishControlsEditor(activity) }, + onReset = { sessionViewModel.resetControlsLayout(activity) }, + modifier = Modifier.fillMaxSize() + ) + + AnimatedVisibility( + visibleState = menuVisibilityState, + enter = fadeIn(tween(220)), + exit = fadeOut(tween(180)), + modifier = Modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.28f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { sessionViewModel.closeMenu(activity) } + ) + ) + } + + AnimatedVisibility( + visibleState = menuVisibilityState, + enter = slideInHorizontally(initialOffsetX = { it / 8 }) + fadeIn(tween(240)), + exit = slideOutHorizontally(targetOffsetX = { it / 8 }) + fadeOut(tween(180)), + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(WindowInsets.displayCutout.asPaddingValues()) + .padding(WindowInsets.statusBars.asPaddingValues()) + .padding(WindowInsets.navigationBars.asPaddingValues()) + .padding(horizontal = 16.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.Top + ) { + PauseDrawer( + activity = activity, + sessionViewModel = sessionViewModel, + settingsViewModel = settingsViewModel, + globalSettingsViewModel = globalSettingsViewModel, + connectedGamepads = connectedGamepads, + selectedTab = selectedTab, + onSelectedTabChange = { + if (it != PauseMenuTab.Settings) { + requestedSettingsScope = null + } + selectedTab = it + }, + onOpenSettingsTab = { + requestedSettingsScope = PauseSettingsScope.CustomConfig + selectedTab = PauseMenuTab.Settings + }, + requestedSettingsScope = requestedSettingsScope, + onSettingsScopeRequestConsumed = { requestedSettingsScope = null }, + onNeedsRestart = { pendingRestartSettings = it }, + onShowHelp = { pendingHelp = it } + ) + } + } + } + + if (uiState.showExitConfirmation) { + AlertDialog( + onDismissRequest = { sessionViewModel.dismissExitConfirmation() }, + title = { Text(stringResource(R.string.emulation_exit_title)) }, + text = { + ApplyDialogDim() + Text(stringResource(R.string.emulation_exit_message)) + }, + confirmButton = { + TextButton(onClick = { sessionViewModel.confirmExit(activity) }) { + Text( + text = stringResource(R.string.emulation_exit), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { sessionViewModel.dismissExitConfirmation() }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } + + pendingRestartSettings?.let { settings -> + AlertDialog( + onDismissRequest = { }, + title = { Text(stringResource(R.string.settings_restart_required_title)) }, + text = { + ApplyDialogDim() + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.settings_restart_required_summary)) + Text( + stringResource( + R.string.settings_restart_required_message, + formatRestartRequiredSettings(context, settings) + ) + ) + } + }, + confirmButton = { + TextButton(onClick = { + pendingRestartSettings = null + activity.restartApp(uiState.titleId, "", "") + }) { + Text(stringResource(R.string.settings_restart_app)) + } + }, + dismissButton = { + TextButton(onClick = { + pendingRestartSettings = null + }) { + Text(stringResource(R.string.settings_restart_later)) + } + } + ) + } + + settingsViewModel.operationResult?.let { result -> + AlertDialog( + onDismissRequest = { settingsViewModel.dismissOperationResult() }, + title = { + Text( + if (result.isError) { + stringResource(R.string.settings_opt_error) + } else { + stringResource(R.string.settings_success_title) + } + ) + }, + text = { + ApplyDialogDim() + Text(result.message) + }, + confirmButton = { + TextButton(onClick = { settingsViewModel.dismissOperationResult() }) { + Text(stringResource(R.string.action_ok)) + } + } + ) + } + + globalSettingsViewModel.operationResult?.let { result -> + AlertDialog( + onDismissRequest = { globalSettingsViewModel.dismissOperationResult() }, + title = { + Text( + if (result.isError) { + stringResource(R.string.settings_opt_error) + } else { + stringResource(R.string.settings_success_title) + } + ) + }, + text = { + ApplyDialogDim() + Text(result.message) + }, + confirmButton = { + TextButton(onClick = { globalSettingsViewModel.dismissOperationResult() }) { + Text(stringResource(R.string.action_ok)) + } + } + ) + } + + pendingHelp?.let { help -> + SettingsHelpSheet(help = help, onDismiss = { pendingHelp = null }) + } +} + +@Composable +private fun PauseDrawer( + activity: Emulator, + sessionViewModel: EmulationSessionViewModel, + settingsViewModel: SettingsViewModel, + globalSettingsViewModel: SettingsViewModel, + connectedGamepads: List, + selectedTab: PauseMenuTab, + onSelectedTabChange: (PauseMenuTab) -> Unit, + onOpenSettingsTab: () -> Unit, + requestedSettingsScope: PauseSettingsScope?, + onSettingsScopeRequestConsumed: () -> Unit, + onNeedsRestart: (List) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val uiState = sessionViewModel.uiState + val scrollState = rememberScrollState() + + LaunchedEffect(uiState.statusMessage) { + if (!uiState.statusMessage.isNullOrBlank()) { + scrollState.animateScrollTo(0) + } + } + + Surface( + modifier = Modifier + .fillMaxHeight() + .widthIn(max = 420.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.96f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)) + ) { + Column( + modifier = Modifier + .fillMaxHeight() + .verticalScroll(scrollState) + .padding(horizontal = 18.dp, vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + PauseHeader( + title = uiState.gameTitle.ifBlank { uiState.titleId.ifBlank { stringResource(R.string.emulation_menu_title) } }, + subtitle = uiState.titleId.ifBlank { stringResource(R.string.emulation_menu_subtitle) }, + onClose = { sessionViewModel.closeMenu(activity) } + ) + + uiState.statusMessage?.let { message -> + PauseStatusBanner(message = message) + } + + PauseTabRow( + selected = selectedTab, + onSelected = onSelectedTabChange + ) + + when (selectedTab) { + PauseMenuTab.Session -> SessionTab( + activity = activity, + sessionViewModel = sessionViewModel, + settingsViewModel = settingsViewModel, + onOpenSettingsTab = onOpenSettingsTab, + onNeedsRestart = onNeedsRestart + ) + PauseMenuTab.Controls -> ControlsTab( + activity = activity, + sessionViewModel = sessionViewModel, + onShowHelp = onShowHelp + ) + PauseMenuTab.Controller -> ControllerTab( + activity = activity, + sessionViewModel = sessionViewModel, + globalSettingsViewModel = globalSettingsViewModel, + connectedGamepads = connectedGamepads, + onShowHelp = onShowHelp + ) + PauseMenuTab.Settings -> EmbeddedSettingsTab( + activity = activity, + sessionViewModel = sessionViewModel, + settingsViewModel = settingsViewModel, + globalSettingsViewModel = globalSettingsViewModel, + titleId = uiState.titleId, + requestedScope = requestedSettingsScope, + onScopeRequestConsumed = onSettingsScopeRequestConsumed, + onSaved = { restartRequired -> + if (restartRequired.isEmpty()) { + sessionViewModel.showStatusMessage(activity.getString(R.string.emulation_settings_saved)) + } else { + onNeedsRestart(restartRequired) + } + }, + onShowHelp = onShowHelp + ) + } + } + } +} + +@Composable +private fun PauseHeader( + title: String, + subtitle: String, + onClose: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(22.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.48f) + ) { + Row( + modifier = Modifier.padding(start = 16.dp, top = 14.dp, end = 8.dp, bottom = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold), + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.emulation_menu_close), + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + } +} + +@Composable +private fun PauseTabRow( + selected: PauseMenuTab, + onSelected: (PauseMenuTab) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + PauseMenuTab.entries.forEach { tab -> + FilterChip( + selected = tab == selected, + onClick = { onSelected(tab) }, + colors = settingsFilterChipColors(), + leadingIcon = { + Icon( + imageVector = tab.icon, + contentDescription = null + ) + }, + label = { + Text( + text = stringResource(tab.labelRes), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + ) + } + } +} + +@Composable +private fun PauseStatusBanner(message: String) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(18.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.78f) + ) { + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp) + ) + } +} + +@Composable +private fun SessionTab( + activity: Emulator, + sessionViewModel: EmulationSessionViewModel, + settingsViewModel: SettingsViewModel, + onOpenSettingsTab: () -> Unit, + onNeedsRestart: (List) -> Unit +) { + val uiState = sessionViewModel.uiState + val settingsLoaded = uiState.titleId.isNotBlank() && settingsViewModel.isLoaded(uiState.titleId) + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + FilledTonalButton( + onClick = { sessionViewModel.togglePause(activity) }, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = if (uiState.isPaused) Icons.Default.PlayArrow else Icons.Default.Pause, + contentDescription = null + ) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text(stringResource(if (uiState.isPaused) R.string.emulation_resume else R.string.emulation_pause)) + } + FilledTonalButton( + onClick = { sessionViewModel.requestExit() }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.AutoMirrored.Filled.ExitToApp, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text( + text = stringResource(R.string.emulation_exit), + color = MaterialTheme.colorScheme.error + ) + } + } + + CustomConfigSection( + settingsLoaded = settingsLoaded, + hasCustomConfig = settingsViewModel.hasCustomConfig, + saving = settingsViewModel.saving, + onSave = { + settingsViewModel.save(forceCustomConfig = true) { restartRequired -> + if (restartRequired.isEmpty()) { + sessionViewModel.showStatusMessage(activity.getString(R.string.emulation_settings_saved)) + } else { + onNeedsRestart(restartRequired) + } + } + }, + onReset = { + settingsViewModel.deleteCustomConfig { restartRequired -> + if (restartRequired.isEmpty()) { + sessionViewModel.showStatusMessage(activity.getString(R.string.emulation_settings_saved)) + } else { + onNeedsRestart(restartRequired) + } + } + }, + onOpenSettings = onOpenSettingsTab + ) + + if (!settingsLoaded) { + SettingsLoadingState(modifier = Modifier.fillMaxWidth()) + } + } +} + +@Composable +private fun CustomConfigSection( + settingsLoaded: Boolean, + hasCustomConfig: Boolean, + saving: Boolean, + onSave: () -> Unit, + onReset: () -> Unit, + onOpenSettings: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.36f), + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(20.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(R.string.emulation_custom_config), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource( + if (hasCustomConfig) { + R.string.emulation_custom_config_active + } else { + R.string.emulation_custom_config_inactive + } + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + FilledTonalButton( + onClick = onSave, + enabled = settingsLoaded && !saving, + modifier = Modifier + .weight(1f) + .height(44.dp) + ) { + Icon(Icons.Default.Save, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 3.dp)) + Text( + text = stringResource( + if (hasCustomConfig) { + R.string.emulation_save_config_short + } else { + R.string.emulation_create_config_short + } + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + OutlinedButton( + onClick = onReset, + enabled = settingsLoaded && hasCustomConfig && !saving, + modifier = Modifier + .weight(1f) + .height(44.dp) + ) { + Icon(Icons.Default.Restore, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 3.dp)) + Text( + text = stringResource(R.string.emulation_reset_config_short), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + OutlinedButton( + onClick = onOpenSettings, + modifier = Modifier + .fillMaxWidth() + .height(44.dp), + enabled = settingsLoaded + ) { + Icon(Icons.Default.Settings, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text(stringResource(R.string.emulation_open_settings_short)) + } + } + } +} + +@Composable +private fun ControlsTab( + activity: Emulator, + sessionViewModel: EmulationSessionViewModel, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val uiState = sessionViewModel.uiState + + OverlaySettingsRows( + overlayConfig = sessionViewModel.currentOverlayConfig(), + onOverlayConfigChange = { updated -> + sessionViewModel.updateOverlayConfig(activity) { updated } + }, + onShowHelp = onShowHelp, + controllerConnected = uiState.controllerConnected, + onStartControlsEditor = { sessionViewModel.startControlsEditor(activity) }, + onResetControlsLayout = { sessionViewModel.resetControlsLayout(activity) } + ) +} + +@Composable +private fun ControllerTab( + activity: Emulator, + sessionViewModel: EmulationSessionViewModel, + globalSettingsViewModel: SettingsViewModel, + connectedGamepads: List, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + if (!globalSettingsViewModel.isLoaded(titleId = null)) { + SettingsLoadingState(modifier = Modifier.fillMaxWidth()) + return + } + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = { globalSettingsViewModel.discardChanges() }, + enabled = globalSettingsViewModel.isDirty && !globalSettingsViewModel.saving, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.settings_discard)) + } + FilledTonalButton( + onClick = { + globalSettingsViewModel.save { _ -> + globalSettingsViewModel.load(titleId = null, force = true) + sessionViewModel.showStatusMessage(activity.getString(R.string.emulation_settings_saved)) + } + }, + enabled = globalSettingsViewModel.isDirty && !globalSettingsViewModel.saving, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Save, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text(stringResource(R.string.settings_save_cd)) + } + } + + ControllerMappingSections( + cfg = globalSettingsViewModel.config, + connectedGamepads = connectedGamepads, + onUpdate = globalSettingsViewModel::update, + onShowHelp = onShowHelp + ) + } +} + +@Composable +private fun EmbeddedSettingsTab( + activity: Emulator, + sessionViewModel: EmulationSessionViewModel, + settingsViewModel: SettingsViewModel, + globalSettingsViewModel: SettingsViewModel, + titleId: String, + requestedScope: PauseSettingsScope?, + onScopeRequestConsumed: () -> Unit, + onSaved: (List) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val customSettingsLoaded = titleId.isNotBlank() && settingsViewModel.isLoaded(titleId) + val globalSettingsLoaded = globalSettingsViewModel.isLoaded(titleId = null) + val customCategories = remember { + settingsCategories(isPerApp = true).filter { it != SettingsCategory.Controls } + } + val globalCategories = remember { + settingsCategories(isPerApp = false).filter { it != SettingsCategory.Controls } + } + var activeScope by remember { mutableStateOf(PauseSettingsScope.Global) } + var selectedCustomCategory by remember { mutableStateOf(SettingsCategory.Gpu) } + var selectedGlobalCategory by remember { mutableStateOf(SettingsCategory.Gpu) } + + LaunchedEffect(requestedScope) { + if (requestedScope != null) { + activeScope = requestedScope + onScopeRequestConsumed() + } + } + + LaunchedEffect(customCategories, selectedCustomCategory) { + if (selectedCustomCategory !in customCategories) { + selectedCustomCategory = customCategories.firstOrNull() ?: SettingsCategory.Core + } + } + + LaunchedEffect(globalCategories, selectedGlobalCategory) { + if (selectedGlobalCategory !in globalCategories) { + selectedGlobalCategory = globalCategories.firstOrNull() ?: SettingsCategory.Core + } + } + + val activeViewModel = if (activeScope == PauseSettingsScope.Global) { + globalSettingsViewModel + } else { + settingsViewModel + } + val categories = if (activeScope == PauseSettingsScope.Global) { + globalCategories + } else { + customCategories + } + val selectedCategory = if (activeScope == PauseSettingsScope.Global) { + selectedGlobalCategory + } else { + selectedCustomCategory + } + val settingsLoaded = if (activeScope == PauseSettingsScope.Global) { + globalSettingsLoaded + } else { + customSettingsLoaded + } + + fun saveActiveSettings() { + if (activeScope == PauseSettingsScope.Global) { + globalSettingsViewModel.save { restartRequired -> + if (!settingsViewModel.hasCustomConfig && !settingsViewModel.isDirty) { + settingsViewModel.load(titleId, force = true) + } + onSaved(restartRequired) + } + } else { + settingsViewModel.save(forceCustomConfig = true, onSaved = onSaved) + } + } + + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + if (!settingsLoaded) { + SettingsLoadingState(modifier = Modifier.fillMaxWidth()) + return@Column + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilterChip( + selected = activeScope == PauseSettingsScope.Global, + onClick = { activeScope = PauseSettingsScope.Global }, + colors = settingsFilterChipColors(), + label = { Text(stringResource(R.string.settings_title)) } + ) + FilterChip( + selected = activeScope == PauseSettingsScope.CustomConfig, + onClick = { activeScope = PauseSettingsScope.CustomConfig }, + colors = settingsFilterChipColors(), + label = { Text(stringResource(R.string.emulation_custom_config)) } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = { activeViewModel.discardChanges() }, + enabled = activeViewModel.isDirty && !activeViewModel.saving, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.settings_discard)) + } + FilledTonalButton( + onClick = ::saveActiveSettings, + enabled = activeViewModel.isDirty && !activeViewModel.saving, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Save, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text(stringResource(R.string.settings_save_cd)) + } + } + + if (activeScope == PauseSettingsScope.CustomConfig) { + SettingsNote( + text = stringResource( + if (settingsViewModel.hasCustomConfig) { + R.string.settings_custom_config_banner + } else { + R.string.emulation_custom_config_inactive + } + ), + color = if (settingsViewModel.hasCustomConfig) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + + SettingsCategoryStrip( + categories = categories, + selectedCategory = selectedCategory, + onCategorySelected = { + if (activeScope == PauseSettingsScope.Global) { + selectedGlobalCategory = it + } else { + selectedCustomCategory = it + } + } + ) + + SettingsCategoryBody( + category = selectedCategory, + cfg = activeViewModel.config, + overlayConfig = sessionViewModel.currentOverlayConfig(), + modulesList = activeViewModel.modulesList, + modulesSearch = activeViewModel.modulesSearch, + onModulesSearchChange = activeViewModel::onModulesSearchChange, + onToggleModule = activeViewModel::toggleModule, + supportedMemoryMappingMask = activeViewModel.supportedMemoryMappingMask, + customDriverLoadStatus = activeViewModel.customDriverLoadStatus, + availableCameras = activeViewModel.availableCameras, + availableAdhocAddresses = activeViewModel.availableAdhocAddresses, + currentStoragePath = activeViewModel.currentStoragePath, + defaultStoragePath = activeViewModel.defaultStoragePath, + onChangeStorageFolder = {}, + onResetStorageFolder = {}, + installedCustomDrivers = activeViewModel.installedCustomDrivers, + customDriverBusy = activeViewModel.customDriverBusy, + onInstallCustomDriver = {}, + onDownloadCustomDriver = {}, + onPickCameraImage = { isFront -> + activity.requestImagePath { path -> + if (path != null) { + globalSettingsViewModel.setCameraImage(isFront, path) + } + } + }, + onRequestRemoveCustomDriver = { _ -> }, + isPerApp = activeScope == PauseSettingsScope.CustomConfig, + showCustomDriverManagement = false, + showCustomDriverSection = false, + allowClearAllCustomConfigs = false, + allowStorageFolderManagement = false, + allowCameraImagePicker = true, + onClearAllCustomConfigs = {}, + onSelectCustomDriver = activeViewModel::selectCustomDriver, + onUpdate = activeViewModel::update, + onOverlayConfigChange = { updated -> + sessionViewModel.updateOverlayConfig(activity) { updated } + }, + onStartControlsEditor = { sessionViewModel.startControlsEditor(activity) }, + onResetControlsLayout = { sessionViewModel.resetControlsLayout(activity) }, + controllerConnected = sessionViewModel.uiState.controllerConnected, + onShowHelp = onShowHelp + ) + + SettingsNote(text = stringResource(R.string.emulation_runtime_note)) + } +} + +@Composable +private fun ControlsEditorBar( + visibleState: MutableTransitionState, + onDone: () -> Unit, + onReset: () -> Unit, + modifier: Modifier = Modifier +) { + AnimatedVisibility( + visibleState = visibleState, + enter = fadeIn(tween(180)), + exit = fadeOut(tween(180)), + modifier = modifier + ) { + OverlayEditorPalette( + onDone = onDone, + onReset = onReset, + modifier = modifier + ) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/NativeImeOverlay.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/NativeImeOverlay.kt new file mode 100644 index 000000000..3cb71554a --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/emulation/NativeImeOverlay.kt @@ -0,0 +1,233 @@ +package org.vita3k.emulator.ui.screens.emulation + +import android.view.View +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.TextButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.Emulator +import org.vita3k.emulator.NativeLib +import org.vita3k.emulator.R +import org.vita3k.emulator.data.NativeImeState +import org.vita3k.emulator.ui.theme.Vita3KTheme +import org.vita3k.emulator.ui.viewmodel.EmulationSessionViewModel + +private val ImeOverlayBackground = Color.Black.copy(alpha = 0.30f) +private val ImeOverlayText = Color.Black.copy(alpha = 0.96f) +private val ImeOverlayPlaceholder = Color.Black.copy(alpha = 0.70f) +private val ImeOverlayAccent = Color.Black.copy(alpha = 0.96f) + +object NativeImeOverlayHost { + @JvmStatic + fun attach( + composeView: ComposeView, + sessionViewModel: EmulationSessionViewModel + ) { + composeView.setContent { + Vita3KTheme { + NativeImeOverlay( + sessionViewModel = sessionViewModel, + hostView = composeView + ) + } + } + } +} + +@Composable +private fun NativeImeOverlay( + sessionViewModel: EmulationSessionViewModel, + hostView: ComposeView +) { + val uiState = sessionViewModel.uiState + val imeState = sessionViewModel.imeState + val context = LocalContext.current + + val state = imeState + val visible = state?.sceImeActive == true + && state.dialogActive.not() + && !uiState.showMenu + && !uiState.isEditingControls + + SideEffect { + hostView.visibility = if (visible) View.VISIBLE else View.GONE + } + + AnimatedVisibility( + visible = visible, + enter = fadeIn(tween(180)) + slideInVertically(initialOffsetY = { -it / 2 }), + exit = fadeOut(tween(140)) + slideOutVertically(targetOffsetY = { -it / 3 }) + ) { + state?.let { current -> + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + .padding(WindowInsets.displayCutout.asPaddingValues()) + .padding(WindowInsets.statusBars.asPaddingValues()) + .padding(horizontal = 16.dp, vertical = 10.dp), + contentAlignment = Alignment.TopCenter + ) { + val gameViewportWidth = minOf(maxWidth, maxHeight * (960f / 544f)) + val preview = buildImePreview(current) + + Surface( + modifier = Modifier.width(gameViewportWidth), + shape = RoundedCornerShape(22.dp), + color = ImeOverlayBackground, + tonalElevation = 0.dp, + shadowElevation = 0.dp + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color.Transparent) + .padding(horizontal = 16.dp, vertical = 10.dp) + ) { + val buttonInset = if (current.enterLabel.isNotBlank()) 96.dp else 0.dp + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(start = buttonInset, end = buttonInset), + contentAlignment = Alignment.Center + ) { + if (preview.text.isBlank()) { + Text( + text = stringResource(R.string.emulation_ime_placeholder), + style = MaterialTheme.typography.titleMedium, + color = ImeOverlayPlaceholder, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } else { + Text( + text = preview, + style = MaterialTheme.typography.titleMedium, + color = ImeOverlayText, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + if (current.enterLabel.isNotBlank()) { + Box( + modifier = Modifier.align(Alignment.CenterEnd) + ) { + TextButton( + onClick = { + val activity = context as? Emulator + if (activity != null) { + activity.completeImeFromKeyboard(null) + } else { + runCatching { NativeLib.submitIme() } + } + }, + modifier = Modifier + .clip(RoundedCornerShape(999.dp)) + .background(ImeOverlayBackground) + ) { + Text( + text = current.enterLabel, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.SemiBold), + color = ImeOverlayText + ) + } + } + } + } + } + } + } + } +} + +private fun buildImePreview(state: NativeImeState): AnnotatedString { + val text = state.text + val caretIndex = state.caretIndex.coerceIn(0, text.length) + val preeditStart = state.preeditStart.coerceIn(0, text.length) + val preeditEnd = (preeditStart + state.preeditLength).coerceIn(preeditStart, text.length) + val caretStyle = SpanStyle( + color = ImeOverlayAccent, + fontWeight = FontWeight.Bold + ) + val preeditStyle = SpanStyle( + color = ImeOverlayAccent, + textDecoration = TextDecoration.Underline + ) + + return buildAnnotatedString { + fun appendCaret(position: Int) { + if (caretIndex == position) { + withStyle(caretStyle) { + append('|') + } + } + } + + if (text.isEmpty()) { + appendCaret(0) + return@buildAnnotatedString + } + + appendCaret(0) + + var cursor = 0 + while (cursor < text.length) { + val segmentEnd = when { + cursor < preeditStart -> preeditStart + cursor < preeditEnd -> preeditEnd + else -> text.length + } + val segment = text.substring(cursor, segmentEnd) + if (cursor >= preeditStart && cursor < preeditEnd) { + withStyle(preeditStyle) { + append(segment) + } + } else { + append(segment) + } + cursor = segmentEnd + appendCaret(cursor) + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/ControllerMappingSettings.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/ControllerMappingSettings.kt new file mode 100644 index 000000000..5bb8919e2 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/ControllerMappingSettings.kt @@ -0,0 +1,594 @@ +package org.vita3k.emulator.ui.screens.settings + +import android.content.Context +import android.view.InputDevice +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.View +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Dialog +import org.vita3k.emulator.ConnectedGamepad +import org.vita3k.emulator.ControllerFamily +import org.vita3k.emulator.R +import org.vita3k.emulator.data.EmulatorConfig +import org.vita3k.emulator.ui.theme.ApplyDialogDim +import kotlin.math.abs +import kotlin.math.roundToInt + +private const val SDL_GAMEPAD_BUTTON_SOUTH = 0 +private const val SDL_GAMEPAD_BUTTON_EAST = 1 +private const val SDL_GAMEPAD_BUTTON_WEST = 2 +private const val SDL_GAMEPAD_BUTTON_NORTH = 3 +private const val SDL_GAMEPAD_BUTTON_BACK = 4 +private const val SDL_GAMEPAD_BUTTON_GUIDE = 5 +private const val SDL_GAMEPAD_BUTTON_START = 6 +private const val SDL_GAMEPAD_BUTTON_LEFT_STICK = 7 +private const val SDL_GAMEPAD_BUTTON_RIGHT_STICK = 8 +private const val SDL_GAMEPAD_BUTTON_LEFT_SHOULDER = 9 +private const val SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER = 10 +private const val SDL_GAMEPAD_BUTTON_DPAD_UP = 11 +private const val SDL_GAMEPAD_BUTTON_DPAD_DOWN = 12 +private const val SDL_GAMEPAD_BUTTON_DPAD_LEFT = 13 +private const val SDL_GAMEPAD_BUTTON_DPAD_RIGHT = 14 + +private const val SDL_GAMEPAD_AXIS_LEFTX = 0 +private const val SDL_GAMEPAD_AXIS_LEFTY = 1 +private const val SDL_GAMEPAD_AXIS_RIGHTX = 2 +private const val SDL_GAMEPAD_AXIS_RIGHTY = 3 +private const val SDL_GAMEPAD_AXIS_LEFT_TRIGGER = 4 +private const val SDL_GAMEPAD_AXIS_RIGHT_TRIGGER = 5 + +private data class ButtonBindingRow( + @StringRes val labelRes: Int, + val bindIndex: Int +) + +private data class AxisBindingRow( + @StringRes val labelRes: Int, + val bindIndex: Int +) + +private data class ManualBindingOption( + val label: String, + val value: Int +) + +private enum class CaptureMode { + Button, + Axis +} + +private val buttonBindingRows = listOf( + ButtonBindingRow(R.string.settings_controls_bind_dpad_up, SDL_GAMEPAD_BUTTON_DPAD_UP), + ButtonBindingRow(R.string.settings_controls_bind_dpad_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN), + ButtonBindingRow(R.string.settings_controls_bind_dpad_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT), + ButtonBindingRow(R.string.settings_controls_bind_dpad_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT), + ButtonBindingRow(R.string.settings_controls_bind_triangle, SDL_GAMEPAD_BUTTON_NORTH), + ButtonBindingRow(R.string.settings_controls_bind_circle, SDL_GAMEPAD_BUTTON_EAST), + ButtonBindingRow(R.string.settings_controls_bind_cross, SDL_GAMEPAD_BUTTON_SOUTH), + ButtonBindingRow(R.string.settings_controls_bind_square, SDL_GAMEPAD_BUTTON_WEST), + ButtonBindingRow(R.string.settings_controls_bind_l1, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), + ButtonBindingRow(R.string.settings_controls_bind_r1, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), + ButtonBindingRow(R.string.settings_controls_bind_l3, SDL_GAMEPAD_BUTTON_LEFT_STICK), + ButtonBindingRow(R.string.settings_controls_bind_r3, SDL_GAMEPAD_BUTTON_RIGHT_STICK), + ButtonBindingRow(R.string.settings_controls_bind_select, SDL_GAMEPAD_BUTTON_BACK), + ButtonBindingRow(R.string.settings_controls_bind_start, SDL_GAMEPAD_BUTTON_START), + ButtonBindingRow(R.string.settings_controls_bind_ps_button, SDL_GAMEPAD_BUTTON_GUIDE) +) + +private val axisBindingRows = listOf( + AxisBindingRow(R.string.settings_controls_bind_left_stick_up, SDL_GAMEPAD_AXIS_LEFTY), + AxisBindingRow(R.string.settings_controls_bind_left_stick_down, SDL_GAMEPAD_AXIS_LEFTY), + AxisBindingRow(R.string.settings_controls_bind_left_stick_left, SDL_GAMEPAD_AXIS_LEFTX), + AxisBindingRow(R.string.settings_controls_bind_left_stick_right, SDL_GAMEPAD_AXIS_LEFTX), + AxisBindingRow(R.string.settings_controls_bind_right_stick_up, SDL_GAMEPAD_AXIS_RIGHTY), + AxisBindingRow(R.string.settings_controls_bind_right_stick_down, SDL_GAMEPAD_AXIS_RIGHTY), + AxisBindingRow(R.string.settings_controls_bind_right_stick_left, SDL_GAMEPAD_AXIS_RIGHTX), + AxisBindingRow(R.string.settings_controls_bind_right_stick_right, SDL_GAMEPAD_AXIS_RIGHTX), + AxisBindingRow(R.string.settings_controls_bind_l2, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), + AxisBindingRow(R.string.settings_controls_bind_r2, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) +) + +@Composable +internal fun ControllerMappingSections( + cfg: EmulatorConfig, + connectedGamepads: List, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val controllerFamily = connectedGamepads.firstOrNull()?.family ?: ControllerFamily.Standard + var pendingButtonBinding by remember { mutableStateOf(null) } + var pendingAxisBinding by remember { mutableStateOf(null) } + + SettingsSectionCard( + title = stringResource(R.string.settings_controls_connected_title), + summary = if (connectedGamepads.isEmpty()) { + stringResource(R.string.settings_controls_connected_none) + } else { + connectedGamepads.joinToString(separator = "\n") { it.name } + }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_connected_title), + body = stringResource(R.string.settings_controls_connected_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) { + if (connectedGamepads.isEmpty()) { + SettingsNote(text = stringResource(R.string.settings_controls_manual_available)) + } + } + + SettingsSectionCard( + title = stringResource(R.string.settings_controls_mapping_buttons_title), + summary = stringResource(R.string.settings_controls_mapping_buttons_desc), + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_mapping_buttons_title), + body = stringResource(R.string.settings_controls_mapping_buttons_help), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) { + buttonBindingRows.forEach { row -> + SettingsActionRow( + title = stringResource(row.labelRes), + value = buttonLabel(controllerFamily, currentButtonBinding(cfg, row.bindIndex)), + onClick = { pendingButtonBinding = row }, + help = null, + onShowHelp = onShowHelp + ) + } + } + + SettingsSectionCard( + title = stringResource(R.string.settings_controls_mapping_axes_title), + summary = stringResource(R.string.settings_controls_mapping_axes_desc), + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_mapping_axes_title), + body = stringResource(R.string.settings_controls_mapping_axes_help), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) { + SettingsNote(text = stringResource(R.string.settings_controls_mapping_axes_note)) + axisBindingRows.forEach { row -> + SettingsActionRow( + title = stringResource(row.labelRes), + value = axisLabel(controllerFamily, currentAxisBinding(cfg, row.bindIndex)), + onClick = { pendingAxisBinding = row }, + help = null, + onShowHelp = onShowHelp + ) + } + } + + SettingsSectionCard( + title = stringResource(R.string.settings_controls_behavior_title), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + SettingsSliderRow( + title = stringResource(R.string.settings_controls_analog_multiplier), + valueLabel = stringResource( + R.string.settings_controls_multiplier_value, + cfg.controllerAnalogMultiplier.coerceIn(0.1f, 2.0f) + ), + value = (cfg.controllerAnalogMultiplier * 100f).coerceIn(10f, 200f), + onValueChange = { + onUpdate { + controllerAnalogMultiplier = (it.roundToInt() / 100f).coerceIn(0.1f, 2.0f) + } + }, + valueRange = 10f..200f, + steps = 189, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_analog_multiplier), + body = stringResource(R.string.settings_controls_analog_multiplier_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_controls_disable_motion), + checked = cfg.disableMotion, + onCheckedChange = { checked -> + onUpdate { disableMotion = checked } + }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_disable_motion), + body = stringResource(R.string.settings_controls_disable_motion_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } + + pendingButtonBinding?.let { row -> + BindingCaptureDialog( + title = stringResource(row.labelRes), + prompt = stringResource(R.string.settings_controls_capture_button_prompt), + options = buttonManualOptions(controllerFamily), + selectedValue = currentButtonBinding(cfg, row.bindIndex), + captureMode = CaptureMode.Button, + connectedGamepads = connectedGamepads, + onDismiss = { pendingButtonBinding = null }, + onSelected = { mappedButton -> + onUpdate { + controllerBinds = controllerBinds.copyOf().also { binds -> + binds[row.bindIndex] = mappedButton + } + } + pendingButtonBinding = null + } + ) + } + + pendingAxisBinding?.let { row -> + BindingCaptureDialog( + title = stringResource(row.labelRes), + prompt = stringResource(R.string.settings_controls_capture_axis_prompt), + options = axisManualOptions(controllerFamily), + selectedValue = currentAxisBinding(cfg, row.bindIndex), + captureMode = CaptureMode.Axis, + connectedGamepads = connectedGamepads, + onDismiss = { pendingAxisBinding = null }, + onSelected = { mappedAxis -> + onUpdate { + controllerAxisBinds = controllerAxisBinds.copyOf().also { binds -> + binds[row.bindIndex] = mappedAxis + } + } + pendingAxisBinding = null + } + ) + } +} + +@Composable +private fun BindingCaptureDialog( + title: String, + prompt: String, + options: List, + selectedValue: Int, + captureMode: CaptureMode, + connectedGamepads: List, + onDismiss: () -> Unit, + onSelected: (Int) -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + ApplyDialogDim() + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier.padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = prompt, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (connectedGamepads.isNotEmpty()) { + Text( + text = connectedGamepads.joinToString(separator = "\n") { it.name }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + CaptureInputSurface( + captureMode = captureMode, + enabled = connectedGamepads.isNotEmpty(), + onCaptured = onSelected + ) + SettingsScrollableChoiceSelector( + title = stringResource(R.string.settings_controls_manual_selection), + options = options.map { it.label }, + selectedIndex = options.indexOfFirst { it.value == selectedValue }.coerceAtLeast(0), + onSelected = { index -> onSelected(options[index].value) }, + summary = options.firstOrNull { it.value == selectedValue }?.label, + help = null, + onShowHelp = {} + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_cancel)) + } + } + } + } + } +} + +@Composable +private fun CaptureInputSurface( + captureMode: CaptureMode, + enabled: Boolean, + onCaptured: (Int) -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f), + shape = MaterialTheme.shapes.large + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = if (enabled) { + stringResource(R.string.settings_controls_capture_listening) + } else { + stringResource(R.string.settings_controls_capture_waiting_for_controller) + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + AndroidView( + factory = { context -> + GamepadCaptureView(context).apply { + this.captureMode = captureMode + this.onCaptured = onCaptured + } + }, + update = { view -> + view.captureMode = captureMode + view.onCaptured = onCaptured + if (enabled) { + view.post { + view.requestFocusFromTouch() + view.requestFocus() + } + } + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + ) + } + } +} + +private class GamepadCaptureView(context: Context) : View(context) { + var captureMode: CaptureMode = CaptureMode.Button + var onCaptured: (Int) -> Unit = {} + + init { + isFocusable = true + isFocusableInTouchMode = true + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + post { + requestFocusFromTouch() + requestFocus() + } + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (captureMode != CaptureMode.Button) { + return super.dispatchKeyEvent(event) + } + if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount != 0 || !isGamepadDevice(event.device)) { + return super.dispatchKeyEvent(event) + } + + val mapped = keyCodeToSdlButton(event.keyCode) ?: return super.dispatchKeyEvent(event) + onCaptured(mapped) + return true + } + + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (captureMode != CaptureMode.Axis || !event.isFromSource(InputDevice.SOURCE_JOYSTICK)) { + return super.onGenericMotionEvent(event) + } + + val mapped = dominantAxisMapping(event) ?: return super.onGenericMotionEvent(event) + onCaptured(mapped) + return true + } +} + +private fun isGamepadDevice(device: InputDevice?): Boolean { + device ?: return false + val sources = device.sources + return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD || + (sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK +} + +private fun dominantAxisMapping(event: MotionEvent): Int? { + val candidates = listOf( + MotionEvent.AXIS_X to SDL_GAMEPAD_AXIS_LEFTX, + MotionEvent.AXIS_Y to SDL_GAMEPAD_AXIS_LEFTY, + MotionEvent.AXIS_Z to SDL_GAMEPAD_AXIS_RIGHTX, + MotionEvent.AXIS_RZ to SDL_GAMEPAD_AXIS_RIGHTY, + MotionEvent.AXIS_RX to SDL_GAMEPAD_AXIS_RIGHTX, + MotionEvent.AXIS_RY to SDL_GAMEPAD_AXIS_RIGHTY, + MotionEvent.AXIS_LTRIGGER to SDL_GAMEPAD_AXIS_LEFT_TRIGGER, + MotionEvent.AXIS_BRAKE to SDL_GAMEPAD_AXIS_LEFT_TRIGGER, + MotionEvent.AXIS_RTRIGGER to SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, + MotionEvent.AXIS_GAS to SDL_GAMEPAD_AXIS_RIGHT_TRIGGER + ) + + var bestValue = 0f + var bestAxis: Int? = null + for ((androidAxis, sdlAxis) in candidates) { + val value = abs(event.getAxisValue(androidAxis)) + if (value > 0.6f && value > bestValue) { + bestValue = value + bestAxis = sdlAxis + } + } + return bestAxis +} + +private fun keyCodeToSdlButton(keyCode: Int): Int? { + return when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A -> SDL_GAMEPAD_BUTTON_SOUTH + KeyEvent.KEYCODE_BUTTON_B -> SDL_GAMEPAD_BUTTON_EAST + KeyEvent.KEYCODE_BUTTON_X -> SDL_GAMEPAD_BUTTON_WEST + KeyEvent.KEYCODE_BUTTON_Y -> SDL_GAMEPAD_BUTTON_NORTH + KeyEvent.KEYCODE_BUTTON_SELECT -> SDL_GAMEPAD_BUTTON_BACK + KeyEvent.KEYCODE_BUTTON_MODE -> SDL_GAMEPAD_BUTTON_GUIDE + KeyEvent.KEYCODE_BUTTON_START -> SDL_GAMEPAD_BUTTON_START + KeyEvent.KEYCODE_BUTTON_THUMBL -> SDL_GAMEPAD_BUTTON_LEFT_STICK + KeyEvent.KEYCODE_BUTTON_THUMBR -> SDL_GAMEPAD_BUTTON_RIGHT_STICK + KeyEvent.KEYCODE_BUTTON_L1 -> SDL_GAMEPAD_BUTTON_LEFT_SHOULDER + KeyEvent.KEYCODE_BUTTON_R1 -> SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER + KeyEvent.KEYCODE_DPAD_UP -> SDL_GAMEPAD_BUTTON_DPAD_UP + KeyEvent.KEYCODE_DPAD_DOWN -> SDL_GAMEPAD_BUTTON_DPAD_DOWN + KeyEvent.KEYCODE_DPAD_LEFT -> SDL_GAMEPAD_BUTTON_DPAD_LEFT + KeyEvent.KEYCODE_DPAD_RIGHT -> SDL_GAMEPAD_BUTTON_DPAD_RIGHT + else -> null + } +} + +private fun currentButtonBinding(cfg: EmulatorConfig, index: Int): Int { + return if (index in cfg.controllerBinds.indices) { + cfg.controllerBinds[index] + } else { + index + } +} + +private fun currentAxisBinding(cfg: EmulatorConfig, index: Int): Int { + return if (index in cfg.controllerAxisBinds.indices) { + cfg.controllerAxisBinds[index] + } else { + index + } +} + +private fun buttonManualOptions(family: ControllerFamily): List = listOf( + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_DPAD_UP), SDL_GAMEPAD_BUTTON_DPAD_UP), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_DPAD_DOWN), SDL_GAMEPAD_BUTTON_DPAD_DOWN), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_DPAD_LEFT), SDL_GAMEPAD_BUTTON_DPAD_LEFT), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_DPAD_RIGHT), SDL_GAMEPAD_BUTTON_DPAD_RIGHT), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_LEFT_STICK), SDL_GAMEPAD_BUTTON_LEFT_STICK), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_RIGHT_STICK), SDL_GAMEPAD_BUTTON_RIGHT_STICK), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_BACK), SDL_GAMEPAD_BUTTON_BACK), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_GUIDE), SDL_GAMEPAD_BUTTON_GUIDE), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_START), SDL_GAMEPAD_BUTTON_START), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_NORTH), SDL_GAMEPAD_BUTTON_NORTH), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_EAST), SDL_GAMEPAD_BUTTON_EAST), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_SOUTH), SDL_GAMEPAD_BUTTON_SOUTH), + ManualBindingOption(buttonLabel(family, SDL_GAMEPAD_BUTTON_WEST), SDL_GAMEPAD_BUTTON_WEST) +) + +private fun axisManualOptions(family: ControllerFamily): List = listOf( + ManualBindingOption(axisLabel(family, SDL_GAMEPAD_AXIS_LEFTX), SDL_GAMEPAD_AXIS_LEFTX), + ManualBindingOption(axisLabel(family, SDL_GAMEPAD_AXIS_LEFTY), SDL_GAMEPAD_AXIS_LEFTY), + ManualBindingOption(axisLabel(family, SDL_GAMEPAD_AXIS_RIGHTX), SDL_GAMEPAD_AXIS_RIGHTX), + ManualBindingOption(axisLabel(family, SDL_GAMEPAD_AXIS_RIGHTY), SDL_GAMEPAD_AXIS_RIGHTY), + ManualBindingOption(axisLabel(family, SDL_GAMEPAD_AXIS_LEFT_TRIGGER), SDL_GAMEPAD_AXIS_LEFT_TRIGGER), + ManualBindingOption(axisLabel(family, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) +) + +private fun buttonLabel(family: ControllerFamily, button: Int): String { + return when (button) { + SDL_GAMEPAD_BUTTON_DPAD_UP -> "D-Pad Up" + SDL_GAMEPAD_BUTTON_DPAD_DOWN -> "D-Pad Down" + SDL_GAMEPAD_BUTTON_DPAD_LEFT -> "D-Pad Left" + SDL_GAMEPAD_BUTTON_DPAD_RIGHT -> "D-Pad Right" + SDL_GAMEPAD_BUTTON_BACK -> when (family) { + ControllerFamily.Xbox -> "Back" + ControllerFamily.Nintendo -> "-" + ControllerFamily.Standard -> "Select" + } + SDL_GAMEPAD_BUTTON_GUIDE -> when (family) { + ControllerFamily.Xbox -> "Guide" + ControllerFamily.Nintendo -> "Home" + ControllerFamily.Standard -> "PS" + } + SDL_GAMEPAD_BUTTON_START -> when (family) { + ControllerFamily.Nintendo -> "+" + else -> "Start" + } + SDL_GAMEPAD_BUTTON_LEFT_STICK -> if (family == ControllerFamily.Standard) "L3" else "LS" + SDL_GAMEPAD_BUTTON_RIGHT_STICK -> if (family == ControllerFamily.Standard) "R3" else "RS" + SDL_GAMEPAD_BUTTON_LEFT_SHOULDER -> when (family) { + ControllerFamily.Xbox -> "LB" + ControllerFamily.Nintendo -> "L" + ControllerFamily.Standard -> "L1" + } + SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER -> when (family) { + ControllerFamily.Xbox -> "RB" + ControllerFamily.Nintendo -> "R" + ControllerFamily.Standard -> "R1" + } + SDL_GAMEPAD_BUTTON_SOUTH -> when (family) { + ControllerFamily.Xbox -> "A" + ControllerFamily.Nintendo -> "B" + ControllerFamily.Standard -> "Cross" + } + SDL_GAMEPAD_BUTTON_EAST -> when (family) { + ControllerFamily.Xbox -> "B" + ControllerFamily.Nintendo -> "A" + ControllerFamily.Standard -> "Circle" + } + SDL_GAMEPAD_BUTTON_WEST -> when (family) { + ControllerFamily.Xbox -> "X" + ControllerFamily.Nintendo -> "Y" + ControllerFamily.Standard -> "Square" + } + SDL_GAMEPAD_BUTTON_NORTH -> when (family) { + ControllerFamily.Xbox -> "Y" + ControllerFamily.Nintendo -> "X" + ControllerFamily.Standard -> "Triangle" + } + else -> "Button $button" + } +} + +private fun axisLabel(family: ControllerFamily, axis: Int): String { + return when (axis) { + SDL_GAMEPAD_AXIS_LEFTX -> "Left X" + SDL_GAMEPAD_AXIS_LEFTY -> "Left Y" + SDL_GAMEPAD_AXIS_RIGHTX -> "Right X" + SDL_GAMEPAD_AXIS_RIGHTY -> "Right Y" + SDL_GAMEPAD_AXIS_LEFT_TRIGGER -> when (family) { + ControllerFamily.Xbox -> "LT" + ControllerFamily.Nintendo -> "ZL" + ControllerFamily.Standard -> "L2" + } + SDL_GAMEPAD_AXIS_RIGHT_TRIGGER -> when (family) { + ControllerFamily.Xbox -> "RT" + ControllerFamily.Nintendo -> "ZR" + ControllerFamily.Standard -> "R2" + } + else -> "Axis $axis" + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlayLayoutEditorDialog.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlayLayoutEditorDialog.kt new file mode 100644 index 000000000..db1a130ff --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlayLayoutEditorDialog.kt @@ -0,0 +1,149 @@ +package org.vita3k.emulator.ui.screens.settings + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.ActivityInfo +import android.os.Build +import android.view.ViewTreeObserver +import android.view.WindowManager +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import org.vita3k.emulator.overlay.InputOverlay +import org.vita3k.emulator.overlay.OverlayConfig +import org.vita3k.emulator.ui.components.OverlayEditorPalette + +@Composable +internal fun OverlayLayoutEditorDialog( + overlayConfig: OverlayConfig, + onDismiss: () -> Unit, + layoutProfileId: String = "" +) { + BackHandler(onBack = onDismiss) + + val activity = LocalContext.current.findActivity() + var overlayView by remember { mutableStateOf(null) } + OverlayEditorWindowEffect(activity = activity) + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + ) { + AndroidView( + factory = { context -> + InputOverlay(context).apply { + overlayView = this + setLayoutProfileId(layoutProfileId) + setAllowVirtualController(false) + setAutoHideEnabled(false) + setIsInEditMode(true) + setScale(overlayConfig.overlayScale / 100f) + setOpacity(overlayConfig.overlayOpacity) + setState(overlayConfig.activeMask()) + } + }, + update = { view -> + overlayView = view + view.setLayoutProfileId(layoutProfileId) + view.setAllowVirtualController(false) + view.setAutoHideEnabled(false) + view.setIsInEditMode(true) + view.setScale(overlayConfig.overlayScale / 100f) + view.setOpacity(overlayConfig.overlayOpacity) + view.setState(overlayConfig.activeMask()) + }, + modifier = Modifier.fillMaxSize() + ) + + OverlayEditorPalette( + onDone = onDismiss, + onReset = { overlayView?.resetButtonPlacement() } + ) + } +} + +@Composable +private fun OverlayEditorWindowEffect(activity: Activity?) { + DisposableEffect(activity) { + if (activity == null) { + return@DisposableEffect onDispose { } + } + + val window = activity.window + val decorView = window.decorView + val previousOrientation = activity.requestedOrientation + val previousAttributes = WindowManager.LayoutParams().also { it.copyFrom(window.attributes) } + val systemBars = WindowInsetsCompat.Type.systemBars() + val barsWereVisible = ViewCompat.getRootWindowInsets(decorView)?.isVisible(systemBars) ?: true + val controller = WindowCompat.getInsetsController(window, decorView) + val focusListener = ViewTreeObserver.OnWindowFocusChangeListener { hasFocus -> + if (hasFocus) { + WindowCompat.getInsetsController(window, decorView)?.hide(systemBars) + } + } + + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + WindowCompat.setDecorFitsSystemWindows(window, false) + controller?.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + ) + controller?.hide(systemBars) + ViewCompat.setOnApplyWindowInsetsListener(decorView) { view, insets -> + if (insets.isVisible(systemBars)) { + WindowCompat.getInsetsController(window, view)?.hide(systemBars) + } + insets + } + if (decorView.viewTreeObserver.isAlive) { + decorView.viewTreeObserver.addOnWindowFocusChangeListener(focusListener) + } + decorView.post { + WindowCompat.getInsetsController(window, decorView)?.hide(systemBars) + ViewCompat.requestApplyInsets(decorView) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val updatedAttributes = window.attributes + updatedAttributes.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + window.attributes = updatedAttributes + } + + onDispose { + ViewCompat.setOnApplyWindowInsetsListener(decorView, null) + if (decorView.viewTreeObserver.isAlive) { + decorView.viewTreeObserver.removeOnWindowFocusChangeListener(focusListener) + } + WindowCompat.setDecorFitsSystemWindows(window, true) + if (barsWereVisible) { + controller?.show(systemBars) + } else { + controller?.hide(systemBars) + } + window.attributes = previousAttributes + activity.requestedOrientation = previousOrientation + } + } +} + +private tailrec fun Context.findActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlaySettings.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlaySettings.kt new file mode 100644 index 000000000..1a695e773 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/OverlaySettings.kt @@ -0,0 +1,217 @@ +package org.vita3k.emulator.ui.screens.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Restore +import androidx.compose.material.icons.filled.TouchApp +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.ConnectedGamepad +import org.vita3k.emulator.R +import org.vita3k.emulator.data.EmulatorConfig +import org.vita3k.emulator.overlay.OverlayConfig + +@Composable +internal fun OverlaySettingsSection( + cfg: EmulatorConfig, + overlayConfig: OverlayConfig, + isPerApp: Boolean, + connectedGamepads: List, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onOverlayConfigChange: (OverlayConfig) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit, + controllerConnected: Boolean = false, + onStartControlsEditor: (() -> Unit)? = null, + onResetControlsLayout: (() -> Unit)? = null +) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + SettingsSectionCard( + title = stringResource(R.string.settings_tab_controls), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + OverlaySettingsRows( + overlayConfig = overlayConfig, + onOverlayConfigChange = onOverlayConfigChange, + onShowHelp = onShowHelp, + controllerConnected = controllerConnected, + onStartControlsEditor = onStartControlsEditor, + onResetControlsLayout = onResetControlsLayout + ) + } + + if (!isPerApp) { + ControllerMappingSections( + cfg = cfg, + connectedGamepads = connectedGamepads, + onUpdate = onUpdate, + onShowHelp = onShowHelp + ) + } + } +} + +@Composable +internal fun OverlaySettingsRows( + overlayConfig: OverlayConfig, + onOverlayConfigChange: (OverlayConfig) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit, + controllerConnected: Boolean = false, + onStartControlsEditor: (() -> Unit)? = null, + onResetControlsLayout: (() -> Unit)? = null +) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + if (onStartControlsEditor != null || onResetControlsLayout != null) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (onStartControlsEditor != null) { + FilledTonalButton( + onClick = onStartControlsEditor, + enabled = overlayConfig.controlsVisible, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.TouchApp, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + androidx.compose.material3.Text(stringResource(R.string.emulation_controls_edit)) + } + } + if (onResetControlsLayout != null) { + OutlinedButton( + onClick = onResetControlsLayout, + enabled = overlayConfig.controlsVisible, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Restore, contentDescription = null) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + androidx.compose.material3.Text(stringResource(R.string.action_reset)) + } + } + } + } + + SettingsToggleRow( + title = stringResource(R.string.settings_controls_show), + checked = overlayConfig.controlsVisible, + onCheckedChange = { onOverlayConfigChange(overlayConfig.withControlsVisible(it)) }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_show), + body = stringResource(R.string.settings_controls_show_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_controls_hide_on_controller), + checked = overlayConfig.hideOverlayWhenControllerConnected, + onCheckedChange = { + onOverlayConfigChange(overlayConfig.copy(hideOverlayWhenControllerConnected = it)) + }, + summary = if (controllerConnected) { + stringResource(R.string.settings_controls_controller_connected) + } else { + stringResource(R.string.settings_controls_controller_disconnected) + }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_hide_on_controller), + body = stringResource(R.string.settings_controls_hide_on_controller_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_controls_l2r2), + checked = overlayConfig.l2r2Visible, + onCheckedChange = { onOverlayConfigChange(overlayConfig.withL2R2Visible(it)) }, + enabled = overlayConfig.controlsVisible, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_l2r2), + body = stringResource(R.string.settings_controls_l2r2_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_controls_l3r3), + checked = overlayConfig.l3r3Visible, + onCheckedChange = { onOverlayConfigChange(overlayConfig.withL3R3Visible(it)) }, + enabled = overlayConfig.controlsVisible, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_l3r3), + body = stringResource(R.string.settings_controls_l3r3_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_controls_touch_switch), + checked = overlayConfig.touchSwitchVisible, + onCheckedChange = { onOverlayConfigChange(overlayConfig.withTouchSwitchVisible(it)) }, + enabled = overlayConfig.controlsVisible, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_touch_switch), + body = stringResource(R.string.settings_controls_touch_switch_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_controls_hide_toggle), + checked = overlayConfig.hideToggleVisible, + onCheckedChange = { onOverlayConfigChange(overlayConfig.withHideToggleVisible(it)) }, + enabled = overlayConfig.controlsVisible, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_hide_toggle), + body = stringResource(R.string.settings_controls_hide_toggle_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_controls_scale), + valueLabel = stringResource(R.string.settings_controls_percent_value, overlayConfig.overlayScale), + value = overlayConfig.overlayScale.toFloat(), + onValueChange = { + onOverlayConfigChange(overlayConfig.copy(overlayScale = it.toInt())) + }, + valueRange = 50f..150f, + steps = 99, + enabled = overlayConfig.controlsVisible, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_scale), + body = stringResource(R.string.settings_controls_scale_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_controls_opacity), + valueLabel = stringResource(R.string.settings_controls_percent_value, overlayConfig.overlayOpacity), + value = overlayConfig.overlayOpacity.toFloat(), + onValueChange = { + onOverlayConfigChange(overlayConfig.copy(overlayOpacity = it.toInt())) + }, + valueRange = 20f..100f, + steps = 79, + enabled = overlayConfig.controlsVisible, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_controls_opacity), + body = stringResource(R.string.settings_controls_opacity_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsComponents.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsComponents.kt new file mode 100644 index 000000000..176e86c9a --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsComponents.kt @@ -0,0 +1,647 @@ +package org.vita3k.emulator.ui.screens.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R + +private val SectionShape = RoundedCornerShape(28.dp) +private val TileShape = RoundedCornerShape(20.dp) + +@Composable +internal fun settingsFilterChipColors() = FilterChipDefaults.filterChipColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f), + selectedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f), + selectedLabelColor = MaterialTheme.colorScheme.primary, + selectedLeadingIconColor = MaterialTheme.colorScheme.primary +) + +@Composable +internal fun SettingsCategoryStrip( + categories: List, + selectedCategory: SettingsCategory, + onCategorySelected: (SettingsCategory) -> Unit, + modifier: Modifier = Modifier +) { + LazyRow( + modifier = modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 0.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(categories, key = { it.name }) { category -> + FilterChip( + selected = category == selectedCategory, + onClick = { onCategorySelected(category) }, + colors = settingsFilterChipColors(), + label = { Text(category.label()) }, + leadingIcon = { + Icon( + imageVector = category.icon, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + ) + } + } +} + +@Composable +internal fun SettingsSectionCard( + title: String, + summary: String? = null, + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = SectionShape, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold) + ) + if (!summary.isNullOrBlank()) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp) + ) + } + } + help?.let { + SettingsHelpButton(help = it, onShowHelp = onShowHelp) + } + } + content() + } + } +} + +@Composable +internal fun SettingsSubsectionTitle( + title: String, + summary: String? = null, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.SemiBold) + ) + if (!summary.isNullOrBlank()) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +internal fun SettingsToggleRow( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + summary: String? = null, + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val valueLabel = summary ?: androidx.compose.ui.res.stringResource( + if (checked) R.string.settings_value_enabled else R.string.settings_value_disabled + ) + SettingsRowContainer( + title = title, + summary = valueLabel, + modifier = modifier, + enabled = enabled, + onClick = { if (enabled) onCheckedChange(!checked) }, + help = help, + onShowHelp = onShowHelp + ) { + Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +internal fun SettingsChoiceSelector( + title: String, + options: List, + selectedIndex: Int, + onSelected: (Int) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + summary: String? = options.getOrNull(selectedIndex.coerceAtLeast(0)), + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val clampedSelectedIndex = if (options.isEmpty()) 0 else selectedIndex.coerceIn(0, options.lastIndex) + Surface( + modifier = modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (enabled) 0.35f else 0.18f), + contentColor = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + shape = TileShape + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant + ) + if (!summary.isNullOrBlank()) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp) + ) + } + } + help?.let { + SettingsHelpButton(help = it, onShowHelp = onShowHelp) + } + } + if (options.isNotEmpty()) { + SettingsChoiceChips( + options = options, + selectedIndex = clampedSelectedIndex, + onSelected = onSelected, + modifier = Modifier.padding(top = 12.dp) + ) + } + } + } +} + +@Composable +internal fun SettingsScrollableChoiceSelector( + title: String, + options: List, + selectedIndex: Int, + onSelected: (Int) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + summary: String? = options.getOrNull(selectedIndex.coerceAtLeast(0)), + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val clampedSelectedIndex = if (options.isEmpty()) 0 else selectedIndex.coerceIn(0, options.lastIndex) + var expanded by remember { mutableStateOf(false) } + Surface( + modifier = modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (enabled) 0.35f else 0.18f), + contentColor = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + shape = TileShape + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant + ) + if (!summary.isNullOrBlank()) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp) + ) + } + } + help?.let { + SettingsHelpButton(help = it, onShowHelp = onShowHelp) + } + } + if (options.isNotEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp) + ) { + OutlinedTextField( + value = options[clampedSelectedIndex], + onValueChange = {}, + readOnly = true, + enabled = enabled, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + textStyle = MaterialTheme.typography.bodyMedium, + trailingIcon = { + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + modifier = Modifier.rotate(if (expanded) 90f else 0f), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = MaterialTheme.colorScheme.onSurface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurface, + disabledTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline, + disabledBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.45f), + focusedTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unfocusedTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .clip(RoundedCornerShape(12.dp)) + .clickable( + enabled = enabled, + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { expanded = true } + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.heightIn(max = 320.dp) + ) { + options.forEachIndexed { index, option -> + DropdownMenuItem( + text = { + Text( + text = option, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + onClick = { + expanded = false + onSelected(index) + } + ) + } + } + } + } + } + } +} + +@Composable +internal fun SettingsActionRow( + title: String, + value: String? = null, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + actionLabel: String? = null, + onActionClick: (() -> Unit)? = null, + actionEnabled: Boolean = true, + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsRowContainer( + title = title, + summary = value, + modifier = modifier, + enabled = enabled, + onClick = onClick, + help = help, + onShowHelp = onShowHelp + ) { + if (!actionLabel.isNullOrBlank() && onActionClick != null) { + TextButton( + onClick = onActionClick, + enabled = enabled && actionEnabled + ) { + Text(actionLabel) + } + } + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +internal fun SettingsSliderRow( + title: String, + valueLabel: String, + value: Float, + onValueChange: (Float) -> Unit, + valueRange: ClosedFloatingPointRange, + modifier: Modifier = Modifier, + steps: Int = 0, + enabled: Boolean = true, + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + Surface( + modifier = modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (enabled) 0.35f else 0.18f), + contentColor = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant, + shape = TileShape + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = valueLabel, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp) + ) + } + help?.let { + SettingsHelpButton(help = it, onShowHelp = onShowHelp) + } + } + androidx.compose.material3.Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = steps, + enabled = enabled, + interactionSource = interactionSource, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) + } + } +} + +@Composable +internal fun SettingsNote( + text: String, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.onSurfaceVariant +) { + Text( + text = text, + style = MaterialTheme.typography.bodySmall, + color = color, + modifier = modifier.fillMaxWidth() + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +internal fun SettingsChoiceChips( + options: List, + selectedIndex: Int, + onSelected: (Int) -> Unit, + modifier: Modifier = Modifier +) { + FlowRow( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + options.forEachIndexed { index, option -> + FilterChip( + selected = index == selectedIndex, + onClick = { onSelected(index) }, + colors = settingsFilterChipColors(), + label = { Text(option) } + ) + } + } +} + +@Composable +internal fun SettingsSearchResultRow( + entry: SettingsSearchEntry, + onOpen: (SettingsCategory) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsActionRow( + title = entry.title, + value = entry.summary, + onClick = { onOpen(entry.category) }, + help = entry.help, + onShowHelp = onShowHelp + ) +} + +@Composable +internal fun SettingsHelpSheet( + help: SettingsHelpEntry, + onDismiss: () -> Unit +) { + val scrollState = rememberScrollState() + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(help.title, style = MaterialTheme.typography.headlineSmall) }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + help.scope.labelRes?.let { labelRes -> + Surface( + color = MaterialTheme.colorScheme.secondaryContainer, + shape = RoundedCornerShape(999.dp) + ) { + Text( + text = androidx.compose.ui.res.stringResource(labelRes), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + } + Text( + text = help.body, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(androidx.compose.ui.res.stringResource(R.string.action_ok)) + } + } + ) +} + +@Composable +internal fun SettingsLoadingState(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } +} + +@Composable +internal fun SettingsHelpButton( + help: SettingsHelpEntry, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + IconButton(onClick = { onShowHelp(help) }) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = androidx.compose.ui.res.stringResource(R.string.settings_cd_show_info), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +private fun SettingsRowContainer( + title: String, + summary: String?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: (() -> Unit)? = null, + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit, + trailing: @Composable RowScope.() -> Unit +) { + val containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (enabled) 0.35f else 0.18f) + val interactionSource = remember { MutableInteractionSource() } + Surface( + modifier = modifier + .fillMaxWidth() + .clip(TileShape) + .then( + if (onClick != null) { + Modifier.clickable( + enabled = enabled, + interactionSource = interactionSource, + indication = null, + onClick = onClick + ) + } else { + Modifier + } + ), + shape = TileShape, + color = containerColor + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant + ) + if (!summary.isNullOrBlank()) { + Text( + text = summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp) + ) + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + help?.let { + SettingsHelpButton(help = it, onShowHelp = onShowHelp) + } + trailing() + } + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsModels.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsModels.kt new file mode 100644 index 000000000..cd874a74e --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsModels.kt @@ -0,0 +1,68 @@ +package org.vita3k.emulator.ui.screens.settings + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.VolumeUp +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.GraphicEq +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material.icons.filled.Public +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.SettingsSuggest +import androidx.compose.material.icons.filled.Speed +import androidx.compose.material.icons.filled.TouchApp +import androidx.compose.material.icons.filled.Tune +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import org.vita3k.emulator.R + +internal enum class SettingsScope(@StringRes val labelRes: Int?) { + Both(labelRes = null), + Global(labelRes = R.string.settings_scope_global), + PerApp(labelRes = R.string.settings_scope_per_app) +} + +internal enum class SettingsCategory( + @StringRes val labelRes: Int, + val icon: ImageVector +) { + Core(R.string.settings_tab_core, Icons.Default.Tune), + Cpu(R.string.settings_tab_cpu, Icons.Default.Speed), + Gpu(R.string.settings_tab_gpu, Icons.Default.GraphicEq), + Audio(R.string.settings_tab_audio, Icons.AutoMirrored.Filled.VolumeUp), + Camera(R.string.settings_tab_camera, Icons.Default.PhotoCamera), + System(R.string.settings_tab_system, Icons.Default.Language), + Controls(R.string.settings_tab_controls, Icons.Default.TouchApp), + Interface(R.string.settings_tab_interface, Icons.Default.Settings), + Emulator(R.string.settings_tab_emulator, Icons.Default.SettingsSuggest), + Network(R.string.settings_tab_network, Icons.Default.Public), + Debug(R.string.settings_tab_debug, Icons.Default.BugReport) +} + +internal fun settingsCategories(isPerApp: Boolean): List { + return if (isPerApp) { + SettingsCategory.entries.filter { it != SettingsCategory.Camera && it != SettingsCategory.Interface } + } else { + SettingsCategory.entries.toList() + } +} + +@Composable +internal fun SettingsCategory.label(): String = stringResource(labelRes) + +internal data class SettingsHelpEntry( + val title: String, + val body: String, + val scope: SettingsScope = SettingsScope.Both +) + +internal data class SettingsSearchEntry( + val category: SettingsCategory, + val title: String, + val summary: String, + val keywords: String, + val help: SettingsHelpEntry? = null +) diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsRoute.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsRoute.kt new file mode 100644 index 000000000..02889609a --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsRoute.kt @@ -0,0 +1,777 @@ +package org.vita3k.emulator.ui.screens.settings + +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.vita3k.emulator.MainActivity +import org.vita3k.emulator.R +import org.vita3k.emulator.data.CustomDriverLoadStatus +import org.vita3k.emulator.data.RestartRequiredSetting +import org.vita3k.emulator.overlay.OverlayConfig +import org.vita3k.emulator.overlay.OverlayConfigStore +import org.vita3k.emulator.overlay.OverlayLayoutStore +import org.vita3k.emulator.ui.CUSTOM_DRIVER_DOWNLOAD_URL +import org.vita3k.emulator.ui.rememberConnectedGamepads +import org.vita3k.emulator.ui.theme.ApplyDialogDim +import org.vita3k.emulator.ui.viewmodel.SettingsViewModel + +private val CUSTOM_DRIVER_MIME_TYPES = arrayOf( + "application/zip", + "application/x-zip-compressed", + "application/octet-stream" +) +private val IMAGE_MIME_TYPES = arrayOf("image/*") + +private fun formatRestartRequiredSettings( + context: android.content.Context, + settings: List +): String = settings.joinToString(separator = "\n- ", prefix = "- ") { setting -> + context.getString(setting.labelResId) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsRoute( + titleId: String?, + appName: String? = null, + viewModel: SettingsViewModel, + onStorageChanged: () -> Unit = {}, + onBack: () -> Unit +) { + LaunchedEffect(titleId) { + if (!viewModel.isLoaded(titleId)) { + viewModel.load(titleId) + } + } + + val context = LocalContext.current + val isPerApp = titleId != null + val settingsLoaded = viewModel.isLoaded(titleId) + val categories = remember(isPerApp) { settingsCategories(isPerApp) } + val scrollState = rememberScrollState() + val overlayLayoutProfileId = titleId.orEmpty() + var overlayConfig by remember(titleId) { mutableStateOf(OverlayConfigStore.load(context)) } + val connectedGamepads = rememberConnectedGamepads(context) + val controllerConnected = connectedGamepads.isNotEmpty() + + var selectedCategory by rememberSaveable(titleId) { mutableStateOf(SettingsCategory.Core) } + var searchQuery by rememberSaveable(titleId) { mutableStateOf("") } + var showDiscardDialog by remember { mutableStateOf(false) } + var showResetDefaultsDialog by remember { mutableStateOf(false) } + var showDeleteCustomConfigDialog by remember { mutableStateOf(false) } + var showClearAllCustomConfigsDialog by remember { mutableStateOf(false) } + var pendingCustomDriverRemoval by remember { mutableStateOf(null) } + var pendingCameraImageTarget by remember { mutableStateOf(null) } + var activeHelp by remember { mutableStateOf(null) } + var searchActive by rememberSaveable(titleId) { mutableStateOf(false) } + var showOverflowMenu by remember { mutableStateOf(false) } + var showOverlayEditor by remember(titleId) { mutableStateOf(false) } + var pendingRestartSettings by remember { mutableStateOf?>(null) } + var closeAfterRestartNotice by remember { mutableStateOf(false) } + + fun requestStorageFolderChange() { + val activity = context as? MainActivity ?: return + activity.requestStorageFolderChange { path -> + path?.let { viewModel.changeStorageFolder(it, onStorageChanged) } + } + } + + fun requestCustomDriverInstall() { + val activity = context as? MainActivity ?: return + activity.requestFilePath(CUSTOM_DRIVER_MIME_TYPES) { path -> + path?.let(viewModel::installCustomDriver) + } + } + + fun closeSearch() { + searchActive = false + searchQuery = "" + } + + fun pickCameraImage(isFront: Boolean) { + val activity = context as? MainActivity ?: return + pendingCameraImageTarget = isFront + activity.requestFilePath(IMAGE_MIME_TYPES) { path -> + val target = pendingCameraImageTarget + pendingCameraImageTarget = null + if (target != null && path != null) { + viewModel.setCameraImage(target, path) + } + } + } + + fun openUrl(url: String) { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + } + + fun tryBack() { + if (viewModel.isDirty) showDiscardDialog = true else onBack() + } + + BackHandler { tryBack() } + + LaunchedEffect(categories, selectedCategory) { + if (selectedCategory !in categories) { + selectedCategory = categories.firstOrNull() ?: SettingsCategory.Core + } + } + + LaunchedEffect(selectedCategory, searchQuery) { + scrollState.scrollTo(0) + } + + Box(modifier = Modifier.fillMaxSize()) { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + navigationIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant + ), + title = { + if (searchActive) { + SettingsTopBarSearchField( + query = searchQuery, + onQueryChange = { searchQuery = it }, + modifier = Modifier.fillMaxWidth() + ) + } else { + Text( + if (isPerApp && appName != null) { + stringResource(R.string.settings_custom_config_label, appName) + } else { + stringResource(R.string.settings_title) + } + ) + } + }, + navigationIcon = { + IconButton(onClick = { tryBack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.action_back) + ) + } + }, + actions = { + if (!searchActive && viewModel.isDirty && !viewModel.saving && !viewModel.customDriverBusy) { + IconButton(onClick = { + viewModel.save(forceCustomConfig = isPerApp) { restartRequired -> + if (restartRequired.isNotEmpty()) { + pendingRestartSettings = restartRequired + } + } + }) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = stringResource(R.string.settings_save_cd) + ) + } + } + IconButton(onClick = { + if (searchActive) { + closeSearch() + } else { + searchActive = true + } + }) { + Icon( + imageVector = if (searchActive) Icons.Default.Close else Icons.Default.Search, + contentDescription = if (searchActive) { + stringResource(R.string.apps_list_cd_close_search) + } else { + stringResource(R.string.apps_list_cd_search) + } + ) + } + SettingsOverflowMenu( + expanded = showOverflowMenu, + onExpandedChange = { showOverflowMenu = it }, + showDeleteCustomConfig = isPerApp && viewModel.hasCustomConfig, + onResetDefaults = { + showOverflowMenu = false + showResetDefaultsDialog = true + }, + onDeleteCustomConfig = { + showOverflowMenu = false + showDeleteCustomConfigDialog = true + } + ) + } + ) + } + ) { padding -> + if (!settingsLoaded) { + SettingsLoadingState( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) + return@Scaffold + } + + val searchEntries = rememberSettingsSearchEntries( + isPerApp = isPerApp, + backendRenderer = viewModel.config.backendRenderer, + showCustomDriverOptions = viewModel.showCustomDriverOptions, + showTurboModeOption = viewModel.showCustomDriverOptions + ) + + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .background(MaterialTheme.colorScheme.background) + ) { + SettingsContentPane( + modifier = Modifier.fillMaxSize(), + titleId = titleId, + isPerApp = isPerApp, + viewModel = viewModel, + overlayConfig = overlayConfig, + supportedMemoryMappingMask = viewModel.supportedMemoryMappingMask, + customDriverLoadStatus = viewModel.customDriverLoadStatus, + availableCameras = viewModel.availableCameras, + availableAdhocAddresses = viewModel.availableAdhocAddresses, + selectedCategory = selectedCategory, + searchQuery = searchQuery, + onCloseSearch = ::closeSearch, + onSelectedCategoryChange = { selectedCategory = it }, + searchEntries = searchEntries, + currentStoragePath = viewModel.currentStoragePath, + scrollState = scrollState, + controllerConnected = controllerConnected, + connectedGamepads = connectedGamepads, + categories = categories, + onChangeStorageFolder = ::requestStorageFolderChange, + onResetStorageFolder = { viewModel.resetStorageFolder(onStorageChanged) }, + onInstallCustomDriver = ::requestCustomDriverInstall, + onDownloadCustomDriver = { openUrl(CUSTOM_DRIVER_DOWNLOAD_URL) }, + onPickCameraImage = ::pickCameraImage, + onRequestRemoveCustomDriver = { pendingCustomDriverRemoval = it }, + onClearAllCustomConfigs = { showClearAllCustomConfigsDialog = true }, + onOverlayConfigChange = { updated -> + overlayConfig = updated.normalized() + OverlayConfigStore.save(context, overlayConfig) + }, + onStartControlsEditor = { + showOverlayEditor = true + }, + onResetControlsLayout = { + OverlayLayoutStore.resetToDefaults(context, overlayLayoutProfileId) + overlayConfig = OverlayConfigStore.load(context) + }, + onShowHelp = { activeHelp = it }, + ) + } + } + + if (showOverlayEditor) { + OverlayLayoutEditorDialog( + overlayConfig = overlayConfig, + layoutProfileId = overlayLayoutProfileId, + onDismiss = { + showOverlayEditor = false + overlayConfig = OverlayConfigStore.load(context) + } + ) + } + } + + if (showDiscardDialog) { + SettingsUnsavedChangesDialog( + saving = viewModel.saving, + onCancel = { showDiscardDialog = false }, + onSaveAndExit = { + showDiscardDialog = false + viewModel.save(forceCustomConfig = isPerApp) { restartRequired -> + if (restartRequired.isEmpty()) { + onBack() + } else { + closeAfterRestartNotice = true + pendingRestartSettings = restartRequired + } + } + }, + onDiscard = { + showDiscardDialog = false + viewModel.discardChanges() + onBack() + } + ) + } + + if (showResetDefaultsDialog) { + AlertDialog( + onDismissRequest = { showResetDefaultsDialog = false }, + title = { Text(stringResource(R.string.settings_confirm_reset_defaults_title)) }, + text = { + ApplyDialogDim() + Text(stringResource(R.string.settings_confirm_reset_defaults_message)) + }, + confirmButton = { + TextButton(onClick = { + showResetDefaultsDialog = false + viewModel.resetToDefaults() + }) { + Text(stringResource(R.string.settings_reset_defaults)) + } + }, + dismissButton = { + TextButton(onClick = { showResetDefaultsDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } + + if (showDeleteCustomConfigDialog) { + AlertDialog( + onDismissRequest = { showDeleteCustomConfigDialog = false }, + title = { Text(stringResource(R.string.settings_confirm_delete_custom_config_title)) }, + text = { + ApplyDialogDim() + Text( + stringResource( + R.string.settings_confirm_delete_custom_config_message, + appName ?: titleId ?: "" + ) + ) + }, + confirmButton = { + TextButton(onClick = { + showDeleteCustomConfigDialog = false + viewModel.deleteCustomConfig { restartRequired -> + if (restartRequired.isNotEmpty()) { + pendingRestartSettings = restartRequired + } + } + }) { + Text( + text = stringResource(R.string.action_delete), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { showDeleteCustomConfigDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } + + if (showClearAllCustomConfigsDialog) { + AlertDialog( + onDismissRequest = { showClearAllCustomConfigsDialog = false }, + title = { Text(stringResource(R.string.settings_confirm_clear_all_title)) }, + text = { + ApplyDialogDim() + Text(stringResource(R.string.settings_confirm_clear_all_message)) + }, + confirmButton = { + TextButton(onClick = { + showClearAllCustomConfigsDialog = false + viewModel.clearAllCustomConfigs() + }) { + Text( + text = stringResource(R.string.action_delete), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { showClearAllCustomConfigsDialog = false }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } + + pendingCustomDriverRemoval?.let { driverName -> + AlertDialog( + onDismissRequest = { pendingCustomDriverRemoval = null }, + title = { Text(stringResource(R.string.settings_confirm_remove_custom_driver_title)) }, + text = { + ApplyDialogDim() + Text(stringResource(R.string.settings_confirm_remove_custom_driver_message, driverName)) + }, + confirmButton = { + TextButton(onClick = { + pendingCustomDriverRemoval = null + viewModel.removeCustomDriver(driverName) + }) { + Text( + text = stringResource(R.string.action_delete), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { pendingCustomDriverRemoval = null }) { + Text(stringResource(R.string.action_cancel)) + } + } + ) + } + + viewModel.operationResult?.let { result -> + AlertDialog( + onDismissRequest = { viewModel.dismissOperationResult() }, + title = { + Text( + if (result.isError) { + stringResource(R.string.settings_opt_error) + } else { + stringResource(R.string.settings_success_title) + } + ) + }, + text = { + ApplyDialogDim() + Text(result.message) + }, + confirmButton = { + TextButton(onClick = { viewModel.dismissOperationResult() }) { + Text(stringResource(R.string.action_ok)) + } + } + ) + } + + pendingRestartSettings?.let { settings -> + AlertDialog( + onDismissRequest = { }, + title = { Text(stringResource(R.string.settings_restart_required_title)) }, + text = { + ApplyDialogDim() + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.settings_restart_required_summary)) + Text( + stringResource( + R.string.settings_restart_required_message, + formatRestartRequiredSettings(context, settings) + ) + ) + } + }, + confirmButton = { + TextButton(onClick = { + pendingRestartSettings = null + val shouldClose = closeAfterRestartNotice + closeAfterRestartNotice = false + if (shouldClose) { + onBack() + } + }) { + Text(stringResource(R.string.action_ok)) + } + } + ) + } + + activeHelp?.let { help -> + SettingsHelpSheet(help = help, onDismiss = { activeHelp = null }) + } +} + +@Composable +private fun SettingsContentPane( + modifier: Modifier, + titleId: String?, + isPerApp: Boolean, + viewModel: SettingsViewModel, + overlayConfig: OverlayConfig, + supportedMemoryMappingMask: Int, + customDriverLoadStatus: CustomDriverLoadStatus, + availableCameras: List, + availableAdhocAddresses: List>, + selectedCategory: SettingsCategory, + searchQuery: String, + onCloseSearch: () -> Unit, + onSelectedCategoryChange: (SettingsCategory) -> Unit, + searchEntries: List, + currentStoragePath: String, + scrollState: androidx.compose.foundation.ScrollState, + controllerConnected: Boolean, + connectedGamepads: List, + categories: List, + onChangeStorageFolder: () -> Unit, + onResetStorageFolder: () -> Unit, + onInstallCustomDriver: () -> Unit, + onDownloadCustomDriver: () -> Unit, + onPickCameraImage: (Boolean) -> Unit, + onRequestRemoveCustomDriver: (String) -> Unit, + onClearAllCustomConfigs: () -> Unit, + onOverlayConfigChange: (OverlayConfig) -> Unit, + onStartControlsEditor: (() -> Unit)?, + onResetControlsLayout: (() -> Unit)?, + onShowHelp: (SettingsHelpEntry) -> Unit, +) { + Column( + modifier = modifier + .verticalScroll(scrollState) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(16.dp) + ) { + if (isPerApp && viewModel.hasCustomConfig) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primaryContainer, + shape = androidx.compose.foundation.shape.RoundedCornerShape(20.dp) + ) { + Text( + text = stringResource(R.string.settings_custom_config_banner), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) + ) + } + } + + SettingsCategoryStrip( + categories = categories, + selectedCategory = selectedCategory, + onCategorySelected = onSelectedCategoryChange + ) + + if (searchQuery.isBlank()) { + SettingsCategoryBody( + category = selectedCategory, + cfg = viewModel.config, + overlayConfig = overlayConfig, + modulesList = viewModel.modulesList, + modulesSearch = viewModel.modulesSearch, + onModulesSearchChange = viewModel::onModulesSearchChange, + onToggleModule = viewModel::toggleModule, + supportedMemoryMappingMask = supportedMemoryMappingMask, + customDriverLoadStatus = customDriverLoadStatus, + availableCameras = availableCameras, + availableAdhocAddresses = availableAdhocAddresses, + currentStoragePath = currentStoragePath, + defaultStoragePath = viewModel.defaultStoragePath, + onChangeStorageFolder = onChangeStorageFolder, + onResetStorageFolder = onResetStorageFolder, + installedCustomDrivers = viewModel.installedCustomDrivers, + customDriverBusy = viewModel.customDriverBusy, + onInstallCustomDriver = onInstallCustomDriver, + onDownloadCustomDriver = onDownloadCustomDriver, + onPickCameraImage = onPickCameraImage, + onRequestRemoveCustomDriver = onRequestRemoveCustomDriver, + isPerApp = isPerApp, + showCustomDriverManagement = viewModel.showCustomDriverOptions, + showTurboModeOption = viewModel.showCustomDriverOptions, + onClearAllCustomConfigs = onClearAllCustomConfigs, + onSelectCustomDriver = viewModel::selectCustomDriver, + onUpdate = viewModel::update, + onOverlayConfigChange = onOverlayConfigChange, + onStartControlsEditor = onStartControlsEditor, + onResetControlsLayout = onResetControlsLayout, + controllerConnected = controllerConnected, + connectedGamepads = connectedGamepads, + onShowHelp = onShowHelp + ) + } else { + SettingsSearchResults( + query = searchQuery, + entries = searchEntries, + onOpen = { + onSelectedCategoryChange(it) + onCloseSearch() + }, + onShowHelp = onShowHelp + ) + } + + if (titleId == null && viewModel.isDirty) { + Spacer(modifier = Modifier.width(1.dp)) + } + } +} + +@Composable +private fun SettingsOverflowMenu( + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + showDeleteCustomConfig: Boolean, + onResetDefaults: () -> Unit, + onDeleteCustomConfig: () -> Unit +) { + Box { + IconButton(onClick = { onExpandedChange(true) }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.settings_more_options) + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { onExpandedChange(false) } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.settings_reset_defaults)) }, + onClick = onResetDefaults + ) + if (showDeleteCustomConfig) { + DropdownMenuItem( + text = { Text(stringResource(R.string.settings_delete_custom_config)) }, + onClick = onDeleteCustomConfig + ) + } + } + } +} + +@Composable +private fun SettingsTopBarSearchField( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier +) { + val textStyle = MaterialTheme.typography.bodyLarge.merge( + TextStyle(color = MaterialTheme.colorScheme.onSurface) + ) + + Surface( + modifier = modifier, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f), + shape = RoundedCornerShape(22.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 40.dp) + .padding(start = 12.dp, end = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 10.dp) + ) { + if (query.isBlank()) { + Text( + text = stringResource(R.string.settings_search_placeholder), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + BasicTextField( + value = query, + onValueChange = onQueryChange, + singleLine = true, + textStyle = textStyle, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + +@Composable +private fun SettingsUnsavedChangesDialog( + saving: Boolean, + onCancel: () -> Unit, + onSaveAndExit: () -> Unit, + onDiscard: () -> Unit +) { + Dialog(onDismissRequest = onCancel) { + ApplyDialogDim() + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 0.dp, + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringResource(R.string.settings_unsaved_title), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_unsaved_message), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onCancel, enabled = !saving) { + Text(stringResource(R.string.action_cancel)) + } + TextButton(onClick = onDiscard, enabled = !saving) { + Text( + text = stringResource(R.string.settings_discard), + color = MaterialTheme.colorScheme.error + ) + } + TextButton(onClick = onSaveAndExit, enabled = !saving) { + Text(stringResource(R.string.settings_save_and_exit)) + } + } + } + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSearch.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSearch.kt new file mode 100644 index 000000000..65dfe3992 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSearch.kt @@ -0,0 +1,188 @@ +package org.vita3k.emulator.ui.screens.settings + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import org.vita3k.emulator.R + +@Composable +internal fun rememberSettingsSearchEntries( + isPerApp: Boolean, + backendRenderer: String, + showCustomDriverOptions: Boolean, + showTurboModeOption: Boolean = showCustomDriverOptions +): List { + val context = LocalContext.current + val configuration = LocalConfiguration.current + + fun help( + @StringRes titleRes: Int, + @StringRes bodyRes: Int, + scope: SettingsScope = SettingsScope.Both + ): SettingsHelpEntry { + return SettingsHelpEntry( + title = context.getString(titleRes), + body = context.getString(bodyRes), + scope = scope + ) + } + + fun entry( + category: SettingsCategory, + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + scope: SettingsScope = SettingsScope.Both, + keywords: String = "" + ): SettingsSearchEntry { + val title = context.getString(titleRes) + val categoryLabel = context.getString(category.labelRes) + val helpEntry = bodyRes?.let { help(titleRes, it, scope) } + val searchableBody = bodyRes?.let { context.getString(it) }.orEmpty() + return SettingsSearchEntry( + category = category, + title = title, + summary = categoryLabel, + keywords = listOf(title, categoryLabel, searchableBody, keywords).joinToString(" "), + help = helpEntry + ) + } + + val isVulkan = remember(backendRenderer) { isVulkanBackend(backendRenderer) } + + return remember(context, configuration, isPerApp, isVulkan, showCustomDriverOptions, showTurboModeOption) { buildList { + add(entry(SettingsCategory.Core, R.string.settings_modules_mode, R.string.settings_modules_automatic_desc)) + add(entry(SettingsCategory.Core, R.string.settings_modules_list_title, keywords = "lle modules firmware")) + add(entry(SettingsCategory.Cpu, R.string.settings_cpu_opt, R.string.settings_cpu_opt_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_backend, R.string.settings_gpu_backend_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_accuracy, R.string.settings_gpu_accuracy_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_vsync, R.string.settings_gpu_vsync_desc)) + if (isVulkan) { + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_disable_surface_sync, R.string.settings_gpu_disable_surface_sync_desc)) + } + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_async_pipeline, R.string.settings_gpu_async_pipeline_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_screen_filter, R.string.settings_gpu_screen_filter_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_resolution, R.string.settings_gpu_resolution_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_anisotropic, R.string.settings_gpu_anisotropic_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_export_textures, R.string.settings_gpu_export_textures_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_texture_format, R.string.settings_gpu_texture_format_desc, keywords = "png dds export textures")) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_import_textures, R.string.settings_gpu_import_textures_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_shader_cache, R.string.settings_gpu_shader_cache_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_spirv_shader, R.string.settings_gpu_spirv_shader_desc)) + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_fps_hack, R.string.settings_gpu_fps_hack_desc, keywords = "fps framerate 30 60 game hack")) + if (!isPerApp && showTurboModeOption) { + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_turbo_mode, R.string.settings_gpu_turbo_mode_desc, scope = SettingsScope.Global, keywords = "gpu clocks adreno thermal")) + } + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_memory_mapping, R.string.settings_gpu_memory_mapping_desc)) + if (showCustomDriverOptions) { + add(entry(SettingsCategory.Gpu, R.string.settings_gpu_custom_driver, R.string.settings_gpu_custom_driver_desc, keywords = "adreno driver zip")) + } + add(entry(SettingsCategory.Audio, R.string.settings_audio_backend, R.string.settings_audio_backend_desc)) + add(entry(SettingsCategory.Audio, R.string.settings_audio_volume, R.string.settings_audio_volume_desc)) + add(entry(SettingsCategory.Audio, R.string.settings_audio_ngs, R.string.settings_audio_ngs_desc)) + if (!isPerApp) { + add(entry(SettingsCategory.Camera, R.string.settings_camera_front, R.string.settings_camera_front_desc, scope = SettingsScope.Global, keywords = "camera source solid color static image")) + add(entry(SettingsCategory.Camera, R.string.settings_camera_back, R.string.settings_camera_back_desc, scope = SettingsScope.Global, keywords = "camera source solid color static image")) + add(entry(SettingsCategory.Camera, R.string.settings_camera_image, R.string.settings_camera_image_desc, scope = SettingsScope.Global, keywords = "front back camera picture image")) + add(entry(SettingsCategory.Camera, R.string.settings_camera_color, R.string.settings_camera_color_desc, scope = SettingsScope.Global, keywords = "front back camera color red green blue alpha")) + } + add(entry(SettingsCategory.System, R.string.settings_system_pstv_mode, R.string.settings_system_pstv_mode_desc)) + if (!isPerApp) { + add(entry(SettingsCategory.System, R.string.settings_system_show_mode, R.string.settings_system_show_mode_desc, scope = SettingsScope.Global)) + add(entry(SettingsCategory.System, R.string.settings_system_demo_mode, R.string.settings_system_demo_mode_desc, scope = SettingsScope.Global)) + } + add(entry(SettingsCategory.System, R.string.settings_system_enter_button, R.string.settings_system_enter_button_desc)) + add(entry(SettingsCategory.System, R.string.settings_system_language, R.string.settings_system_language_desc)) + add(entry(SettingsCategory.System, R.string.settings_system_date_format, R.string.settings_system_date_format_desc)) + add(entry(SettingsCategory.System, R.string.settings_system_time_format, R.string.settings_system_time_format_desc)) + add(entry(SettingsCategory.System, R.string.settings_system_ime_langs, R.string.settings_system_ime_langs_desc)) + add(entry(SettingsCategory.Controls, R.string.settings_controls_show, R.string.settings_controls_show_desc, scope = SettingsScope.Global, keywords = "overlay touch buttons")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_hide_on_controller, R.string.settings_controls_hide_on_controller_desc, scope = SettingsScope.Global, keywords = "gamepad controller overlay hide")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_l2r2, R.string.settings_controls_l2r2_desc, scope = SettingsScope.Global, keywords = "touch triggers shoulder buttons")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_l3r3, R.string.settings_controls_l3r3_desc, scope = SettingsScope.Global, keywords = "touch stick clicks l3 r3")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_touch_switch, R.string.settings_controls_touch_switch_desc, scope = SettingsScope.Global, keywords = "front rear touchscreen switch")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_hide_toggle, R.string.settings_controls_hide_toggle_desc, scope = SettingsScope.Global, keywords = "hide overlay button toggle")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_scale, R.string.settings_controls_scale_desc, scope = SettingsScope.Global, keywords = "overlay size")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_opacity, R.string.settings_controls_opacity_desc, scope = SettingsScope.Global, keywords = "overlay transparency")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_mapping_buttons_title, R.string.settings_controls_mapping_buttons_desc, scope = SettingsScope.Global, keywords = "controller remap mapping buttons gamepad")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_mapping_axes_title, R.string.settings_controls_mapping_axes_desc, scope = SettingsScope.Global, keywords = "controller remap sticks triggers axes gamepad")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_analog_multiplier, R.string.settings_controls_analog_multiplier_desc, scope = SettingsScope.Global, keywords = "analog stick sensitivity multiplier")) + add(entry(SettingsCategory.Controls, R.string.settings_controls_disable_motion, R.string.settings_controls_disable_motion_desc, scope = SettingsScope.Global, keywords = "gyro accelerometer motion controls")) + add(entry(SettingsCategory.Network, R.string.settings_network_psn_signed_in, R.string.settings_network_psn_signed_in_desc, scope = SettingsScope.PerApp)) + if (!isPerApp) { + add(entry(SettingsCategory.Network, R.string.settings_network_http_enable, R.string.settings_network_http_enable_desc, scope = SettingsScope.Global)) + add(entry(SettingsCategory.Network, R.string.settings_network_http_timeout_attempts, R.string.settings_network_http_timeout_attempts_desc, scope = SettingsScope.Global, keywords = "retry timeout http")) + add(entry(SettingsCategory.Network, R.string.settings_network_http_timeout_sleep, R.string.settings_network_http_timeout_sleep_desc, scope = SettingsScope.Global, keywords = "retry delay timeout http ms")) + add(entry(SettingsCategory.Network, R.string.settings_network_http_read_end_attempts, R.string.settings_network_http_read_end_attempts_desc, scope = SettingsScope.Global, keywords = "read end retry http")) + add(entry(SettingsCategory.Network, R.string.settings_network_http_read_end_sleep, R.string.settings_network_http_read_end_sleep_desc, scope = SettingsScope.Global, keywords = "read end delay http ms")) + add(entry(SettingsCategory.Network, R.string.settings_network_adhoc_address, R.string.settings_network_adhoc_address_desc, scope = SettingsScope.Global, keywords = "adhoc subnet local address")) + } + if (!isPerApp) { + add(entry(SettingsCategory.Debug, R.string.settings_debug_log_imports, R.string.settings_debug_log_imports_desc, scope = SettingsScope.Global, keywords = "import symbols hle")) + add(entry(SettingsCategory.Debug, R.string.settings_debug_log_exports, R.string.settings_debug_log_exports_desc, scope = SettingsScope.Global, keywords = "export symbols hle")) + } + add(entry(SettingsCategory.Debug, R.string.settings_debug_log_active_shaders, R.string.settings_debug_log_active_shaders_desc)) + add(entry(SettingsCategory.Debug, R.string.settings_debug_log_uniforms, R.string.settings_debug_log_uniforms_desc)) + add(entry(SettingsCategory.Debug, R.string.settings_debug_color_surface, R.string.settings_debug_color_surface_desc)) + add(entry(SettingsCategory.Debug, R.string.settings_debug_validation_layer, R.string.settings_debug_validation_layer_desc)) + if (!isPerApp) { + add(entry(SettingsCategory.Debug, R.string.settings_debug_dump_elfs, R.string.settings_debug_dump_elfs_desc, scope = SettingsScope.Global, keywords = "elf dump loaded code")) + } + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_texture_cache, R.string.settings_emulator_texture_cache_desc)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_stretch_display, R.string.settings_emulator_stretch_display_desc)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_pixel_perfect, R.string.settings_emulator_pixel_perfect_desc)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_file_loading_delay, R.string.settings_emulator_file_loading_delay_desc)) + if (!isPerApp) { + add(entry(SettingsCategory.Interface, R.string.settings_emulator_ui_language, R.string.settings_emulator_ui_language_desc, scope = SettingsScope.Global, keywords = "interface ui app language locale")) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_storage_folder, R.string.settings_emulator_storage_folder_desc, scope = SettingsScope.Global, keywords = "storage folder path data directory")) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_show_compile_shaders, R.string.settings_emulator_show_compile_shaders_desc, scope = SettingsScope.Global)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_check_updates, R.string.settings_emulator_check_updates_desc, scope = SettingsScope.Global)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_archive_log, R.string.settings_emulator_archive_log_desc, scope = SettingsScope.Global)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_log_compat_warn, R.string.settings_emulator_log_compat_warn_desc, scope = SettingsScope.Global)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_log_level, R.string.settings_emulator_log_level_desc, scope = SettingsScope.Global, keywords = "logging trace debug info warning error critical off")) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_perf_overlay, R.string.settings_emulator_perf_overlay_desc, scope = SettingsScope.Global)) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_perf_overlay_detail, R.string.settings_emulator_perf_overlay_detail_desc, scope = SettingsScope.Global, keywords = "minimum low medium maximum")) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_perf_overlay_position, R.string.settings_emulator_perf_overlay_position_desc, scope = SettingsScope.Global, keywords = "top bottom left center right")) + add(entry(SettingsCategory.Emulator, R.string.settings_emulator_screenshot_format, R.string.settings_emulator_screenshot_format_desc, scope = SettingsScope.Global, keywords = "jpeg png none")) + } + } } +} + +@Composable +internal fun SettingsSearchResults( + query: String, + entries: List, + onOpen: (SettingsCategory) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val normalizedQuery = remember(query) { normalizeSettingsSearchToken(query) } + val filtered = remember(entries, normalizedQuery) { + entries.filter { entry -> + normalizeSettingsSearchToken(entry.keywords).contains(normalizedQuery) + } + } + + SettingsSectionCard( + title = stringResource(R.string.settings_search_results), + summary = if (filtered.isEmpty()) stringResource(R.string.settings_search_no_results) else null, + help = null, + onShowHelp = onShowHelp + ) { + if (filtered.isEmpty()) { + SettingsNote(text = stringResource(R.string.settings_search_no_results)) + } else { + filtered.forEach { entry -> + SettingsSearchResultRow(entry = entry, onOpen = onOpen, onShowHelp = onShowHelp) + } + } + } +} + +private fun normalizeSettingsSearchToken(value: String): String { + return value + .lowercase() + .replace(Regex("[^a-z0-9]+"), " ") + .trim() +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSections.kt b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSections.kt new file mode 100644 index 000000000..871f755ca --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/screens/settings/SettingsSections.kt @@ -0,0 +1,1771 @@ +package org.vita3k.emulator.ui.screens.settings + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.vita3k.emulator.R +import org.vita3k.emulator.ConnectedGamepad +import org.vita3k.emulator.data.CustomDriverLoadStatus +import org.vita3k.emulator.data.EmulatorConfig +import org.vita3k.emulator.data.FirmwareLinks +import org.vita3k.emulator.data.UiLanguages +import org.vita3k.emulator.overlay.OverlayConfig +import kotlin.math.roundToInt + +private data class ImeLangEntry(val nameResId: Int, val flag: Long) + +private val IME_LANGS = listOf( + ImeLangEntry(R.string.settings_ime_danish, 0x00000001L), + ImeLangEntry(R.string.settings_ime_german, 0x00000002L), + ImeLangEntry(R.string.settings_ime_english_us, 0x00000004L), + ImeLangEntry(R.string.settings_ime_spanish, 0x00000008L), + ImeLangEntry(R.string.settings_ime_french, 0x00000010L), + ImeLangEntry(R.string.settings_ime_italian, 0x00000020L), + ImeLangEntry(R.string.settings_ime_dutch, 0x00000040L), + ImeLangEntry(R.string.settings_ime_norwegian, 0x00000080L), + ImeLangEntry(R.string.settings_ime_polish, 0x00000100L), + ImeLangEntry(R.string.settings_ime_portuguese_pt, 0x00000200L), + ImeLangEntry(R.string.settings_ime_russian, 0x00000400L), + ImeLangEntry(R.string.settings_ime_finnish, 0x00000800L), + ImeLangEntry(R.string.settings_ime_swedish, 0x00001000L), + ImeLangEntry(R.string.settings_ime_japanese, 0x00002000L), + ImeLangEntry(R.string.settings_ime_korean, 0x00004000L), + ImeLangEntry(R.string.settings_ime_chinese_simp, 0x00008000L), + ImeLangEntry(R.string.settings_ime_chinese_trad, 0x00010000L), + ImeLangEntry(R.string.settings_ime_portuguese_br, 0x00020000L), + ImeLangEntry(R.string.settings_ime_english_gb, 0x00040000L), + ImeLangEntry(R.string.settings_ime_turkish, 0x00080000L) +) + +private const val CAMERA_SOURCE_SOLID = 0 +private const val CAMERA_SOURCE_IMAGE = 1 +private const val CAMERA_SOURCE_DEVICE = 2 + +private data class CameraSectionState( + val isFront: Boolean, + val title: String, + val description: String, + val sourceType: Int, + val sourceId: String, + val imagePath: String, + val colorValue: Long +) + +private fun helpEntry( + title: String, + body: String, + scope: SettingsScope = SettingsScope.Both +): SettingsHelpEntry = SettingsHelpEntry(title = title, body = body, scope = scope) + +internal fun isVulkanBackend(backendRenderer: String): Boolean = + backendRenderer.equals("Vulkan", ignoreCase = true) + +private fun cameraSourceIndex(type: Int, id: String, availableCameras: List): Int { + return when { + type == CAMERA_SOURCE_SOLID -> 0 + type == CAMERA_SOURCE_IMAGE -> 1 + availableCameras.isNotEmpty() -> availableCameras.indexOf(id).takeIf { it >= 0 }?.plus(2) ?: 2 + else -> 2 + } +} + +private fun cameraSourceValue( + type: Int, + id: String, + availableCameras: List, + solidLabel: String, + staticLabel: String +): String? { + return when { + type == CAMERA_SOURCE_SOLID -> solidLabel + type == CAMERA_SOURCE_IMAGE -> staticLabel + availableCameras.isNotEmpty() -> id.takeIf { it in availableCameras } ?: availableCameras.first() + else -> null + } +} + +private fun normalizedCameraColor(color: Long): Long = + 0xFF000000L or (color and 0x00FF_FFFFL) + +private fun cameraColor(color: Long): Color { + val normalized = normalizedCameraColor(color) + val red = ((normalized shr 16) and 0xFF).toInt() + val green = ((normalized shr 8) and 0xFF).toInt() + val blue = (normalized and 0xFF).toInt() + return Color( + red = red / 255f, + green = green / 255f, + blue = blue / 255f, + alpha = 1f + ) +} + +private fun cameraColorHex(color: Long): String = String.format("#%06X", color and 0x00FF_FFFFL) + +private fun cameraColorChannel(color: Long, shift: Int): Int = + ((normalizedCameraColor(color) shr shift) and 0xFF).toInt() + +private fun cameraColorValue(red: Int, green: Int, blue: Int): Long = + 0xFF000000L or + ((red.coerceIn(0, 255).toLong() and 0xFF) shl 16) or + ((green.coerceIn(0, 255).toLong() and 0xFF) shl 8) or + (blue.coerceIn(0, 255).toLong() and 0xFF) + +private fun cameraImageValue(path: String, fallback: String): String { + if (path.isBlank()) return fallback + val fileName = path.substringAfterLast('\\').substringAfterLast('/') + return fileName.ifBlank { path } +} + +@Composable +private fun SettingsChoiceField( + title: String, + options: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, + enabled: Boolean = true, + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsChoiceSelector( + title = title, + options = options, + selectedIndex = selectedIndex, + onSelected = onSelect, + enabled = enabled, + help = help, + onShowHelp = onShowHelp + ) +} + +@Composable +private fun SettingsScrollableChoiceField( + title: String, + options: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, + enabled: Boolean = true, + help: SettingsHelpEntry? = null, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsScrollableChoiceSelector( + title = title, + options = options, + selectedIndex = selectedIndex, + onSelected = onSelect, + enabled = enabled, + help = help, + onShowHelp = onShowHelp + ) +} + +@Composable +internal fun SettingsCategoryBody( + category: SettingsCategory, + cfg: EmulatorConfig, + overlayConfig: OverlayConfig, + modulesList: List>, + modulesSearch: String, + onModulesSearchChange: (String) -> Unit, + onToggleModule: (String) -> Unit, + supportedMemoryMappingMask: Int, + customDriverLoadStatus: CustomDriverLoadStatus, + availableCameras: List, + availableAdhocAddresses: List>, + currentStoragePath: String, + defaultStoragePath: String, + onChangeStorageFolder: () -> Unit, + onResetStorageFolder: () -> Unit, + installedCustomDrivers: List, + customDriverBusy: Boolean, + onInstallCustomDriver: () -> Unit, + onDownloadCustomDriver: () -> Unit, + onPickCameraImage: (Boolean) -> Unit, + onRequestRemoveCustomDriver: (String) -> Unit, + isPerApp: Boolean, + showCustomDriverManagement: Boolean = true, + showCustomDriverSection: Boolean = showCustomDriverManagement, + showTurboModeOption: Boolean = showCustomDriverManagement, + allowClearAllCustomConfigs: Boolean = true, + allowStorageFolderManagement: Boolean = true, + allowCameraImagePicker: Boolean = true, + onClearAllCustomConfigs: () -> Unit, + onSelectCustomDriver: (String) -> Unit, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onOverlayConfigChange: (OverlayConfig) -> Unit, + onStartControlsEditor: (() -> Unit)? = null, + onResetControlsLayout: (() -> Unit)? = null, + controllerConnected: Boolean = false, + connectedGamepads: List = emptyList(), + onShowHelp: (SettingsHelpEntry) -> Unit +) { + when (category) { + SettingsCategory.Core -> CoreSettingsSection( + cfg = cfg, + modulesList = modulesList, + modulesSearch = modulesSearch, + onModulesSearchChange = onModulesSearchChange, + onToggleModule = onToggleModule, + onUpdate = onUpdate, + onShowHelp = onShowHelp + ) + + SettingsCategory.Cpu -> CpuSettingsSection(cfg = cfg, onUpdate = onUpdate, onShowHelp = onShowHelp) + SettingsCategory.Gpu -> GpuSettingsSection( + cfg = cfg, + isPerApp = isPerApp, + supportedMemoryMappingMask = supportedMemoryMappingMask, + customDriverLoadStatus = customDriverLoadStatus, + installedCustomDrivers = installedCustomDrivers, + customDriverBusy = customDriverBusy, + showCustomDriverManagement = showCustomDriverManagement, + showCustomDriverSection = showCustomDriverSection, + showTurboModeOption = showTurboModeOption, + onInstallCustomDriver = onInstallCustomDriver, + onDownloadCustomDriver = onDownloadCustomDriver, + onRequestRemoveCustomDriver = onRequestRemoveCustomDriver, + onSelectCustomDriver = onSelectCustomDriver, + onUpdate = onUpdate, + onShowHelp = onShowHelp + ) + + SettingsCategory.Audio -> AudioSettingsSection(cfg = cfg, onUpdate = onUpdate, onShowHelp = onShowHelp) + SettingsCategory.Camera -> CameraSettingsSection( + cfg = cfg, + availableCameras = availableCameras, + onUpdate = onUpdate, + onShowHelp = onShowHelp, + onPickCameraImage = onPickCameraImage, + allowImagePicker = allowCameraImagePicker + ) + SettingsCategory.System -> SystemSettingsSection(cfg = cfg, isPerApp = isPerApp, onUpdate = onUpdate, onShowHelp = onShowHelp) + SettingsCategory.Controls -> OverlaySettingsSection( + cfg = cfg, + overlayConfig = overlayConfig, + isPerApp = isPerApp, + connectedGamepads = connectedGamepads, + onUpdate = onUpdate, + onOverlayConfigChange = onOverlayConfigChange, + onShowHelp = onShowHelp, + controllerConnected = controllerConnected, + onStartControlsEditor = onStartControlsEditor, + onResetControlsLayout = onResetControlsLayout + ) + SettingsCategory.Interface -> InterfaceSettingsSection( + cfg = cfg, + isPerApp = isPerApp, + onUpdate = onUpdate, + onShowHelp = onShowHelp + ) + SettingsCategory.Network -> NetworkSettingsSection( + cfg = cfg, + isPerApp = isPerApp, + availableAdhocAddresses = availableAdhocAddresses, + onUpdate = onUpdate, + onShowHelp = onShowHelp + ) + SettingsCategory.Debug -> DebugSettingsSection( + cfg = cfg, + isPerApp = isPerApp, + onUpdate = onUpdate, + onShowHelp = onShowHelp + ) + SettingsCategory.Emulator -> EmulatorSettingsSection( + cfg = cfg, + isPerApp = isPerApp, + currentStoragePath = currentStoragePath, + defaultStoragePath = defaultStoragePath, + onChangeStorageFolder = onChangeStorageFolder, + onResetStorageFolder = onResetStorageFolder, + allowStorageFolderManagement = allowStorageFolderManagement, + allowClearAllCustomConfigs = allowClearAllCustomConfigs, + onClearAllCustomConfigs = onClearAllCustomConfigs, + onUpdate = onUpdate, + onShowHelp = onShowHelp + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun CoreSettingsSection( + cfg: EmulatorConfig, + modulesList: List>, + modulesSearch: String, + onModulesSearchChange: (String) -> Unit, + onToggleModule: (String) -> Unit, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val moduleModeHelpBody = listOf( + "${stringResource(R.string.settings_modules_automatic)}: ${stringResource(R.string.settings_modules_automatic_desc)}", + "${stringResource(R.string.settings_modules_auto_manual).replace("+", " + ")}: ${stringResource(R.string.settings_modules_auto_manual_desc)}", + "${stringResource(R.string.settings_modules_manual)}: ${stringResource(R.string.settings_modules_manual_desc)}" + ).joinToString("\n\n") + + SettingsSectionCard( + title = stringResource(R.string.settings_core_module_management), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + SettingsChoiceSelector( + title = stringResource(R.string.settings_modules_mode), + options = listOf( + stringResource(R.string.settings_modules_automatic), + stringResource(R.string.settings_modules_auto_manual), + stringResource(R.string.settings_modules_manual) + ), + selectedIndex = cfg.modulesMode.coerceIn(0, 2), + onSelected = { index -> onUpdate { modulesMode = index } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_modules_mode), + body = moduleModeHelpBody + ), + onShowHelp = onShowHelp + ) + + SettingsSubsectionTitle( + title = stringResource(R.string.settings_modules_list_title) + ) + + if (modulesList.isEmpty()) { + SettingsNote(text = stringResource(R.string.settings_modules_no_firmware)) + } else { + androidx.compose.material3.OutlinedTextField( + value = modulesSearch, + onValueChange = onModulesSearchChange, + placeholder = { Text(stringResource(R.string.settings_modules_search)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + enabled = cfg.modulesMode != 0 + ) + val filteredModules = modulesList.filter { + modulesSearch.isBlank() || it.first.contains(modulesSearch, ignoreCase = true) + } + if (filteredModules.isEmpty()) { + SettingsNote(text = stringResource(R.string.settings_search_no_results)) + } else { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.24f), + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(20.dp) + ) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + filteredModules.forEach { (name, enabled) -> + FilterChip( + selected = enabled, + onClick = { onToggleModule(name) }, + enabled = cfg.modulesMode != 0, + colors = settingsFilterChipColors(), + label = { + Text( + text = name, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + ) + } + } + } + } + } + + } +} + + @Composable + private fun CpuSettingsSection( + cfg: EmulatorConfig, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit + ) { + SettingsSectionCard( + title = stringResource(R.string.settings_tab_cpu), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + SettingsSubsectionTitle(title = stringResource(R.string.settings_cpu_dynarmic_settings)) + SettingsToggleRow( + title = stringResource(R.string.settings_cpu_opt), + checked = cfg.cpuOpt, + onCheckedChange = { onUpdate { cpuOpt = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_cpu_opt), + body = stringResource(R.string.settings_cpu_opt_desc) + ), + onShowHelp = onShowHelp + ) + } + } + + @OptIn(ExperimentalLayoutApi::class) + @Composable +private fun GpuSettingsSection( + cfg: EmulatorConfig, + isPerApp: Boolean, + supportedMemoryMappingMask: Int, + customDriverLoadStatus: CustomDriverLoadStatus, + installedCustomDrivers: List, + customDriverBusy: Boolean, + showCustomDriverManagement: Boolean, + showCustomDriverSection: Boolean, + showTurboModeOption: Boolean, + onInstallCustomDriver: () -> Unit, + onDownloadCustomDriver: () -> Unit, + onRequestRemoveCustomDriver: (String) -> Unit, + onSelectCustomDriver: (String) -> Unit, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit + ) { + val isVulkan = isVulkanBackend(cfg.backendRenderer) + + SettingsSectionCard( + title = stringResource(R.string.settings_gpu_renderer_section), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + val backendTitle = stringResource(R.string.settings_gpu_backend) + val backendHelp = helpEntry(backendTitle, stringResource(R.string.settings_gpu_backend_desc)) + val backendOptions = listOf("OpenGL", "Vulkan") + SettingsChoiceField( + title = backendTitle, + options = backendOptions, + selectedIndex = backendOptions.indexOf(cfg.backendRenderer).coerceAtLeast(0), + onSelect = { index -> onUpdate { backendRenderer = backendOptions[index] } }, + help = backendHelp, + onShowHelp = onShowHelp + ) + val screenFilterTitle = stringResource(R.string.settings_gpu_screen_filter) + val screenFilterHelp = helpEntry(screenFilterTitle, stringResource(R.string.settings_gpu_screen_filter_desc)) + val filterOptions = if (isVulkan) { + listOf("Nearest", "Bilinear", "Bicubic", "FXAA", "FSR") + } else { + listOf("Bilinear", "FXAA") + } + val selectedFilter = cfg.screenFilter.takeIf { it in filterOptions } ?: filterOptions.first() + SettingsChoiceField( + title = screenFilterTitle, + options = filterOptions, + selectedIndex = filterOptions.indexOf(selectedFilter).coerceAtLeast(0), + onSelect = { index -> onUpdate { screenFilter = filterOptions[index] } }, + help = screenFilterHelp, + onShowHelp = onShowHelp + ) + if (isVulkan) { + val accuracyTitle = stringResource(R.string.settings_gpu_accuracy) + val accuracyHelp = helpEntry(accuracyTitle, stringResource(R.string.settings_gpu_accuracy_desc)) + val accuracyOptions = listOf( + stringResource(R.string.settings_gpu_accuracy_standard), + stringResource(R.string.settings_gpu_accuracy_high) + ) + SettingsChoiceField( + title = accuracyTitle, + options = accuracyOptions, + selectedIndex = if (cfg.highAccuracy) 1 else 0, + onSelect = { index -> onUpdate { highAccuracy = index == 1 } }, + help = accuracyHelp, + onShowHelp = onShowHelp + ) + } + if (!isVulkan) { + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_vsync), + checked = cfg.vSync, + onCheckedChange = { onUpdate { vSync = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_vsync), + body = stringResource(R.string.settings_gpu_vsync_desc) + ), + onShowHelp = onShowHelp + ) + } + if (isVulkan) { + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_disable_surface_sync), + checked = cfg.disableSurfaceSync, + onCheckedChange = { onUpdate { disableSurfaceSync = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_disable_surface_sync), + body = stringResource(R.string.settings_gpu_disable_surface_sync_desc) + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_async_pipeline), + checked = cfg.asyncPipelineCompilation, + onCheckedChange = { onUpdate { asyncPipelineCompilation = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_async_pipeline), + body = stringResource(R.string.settings_gpu_async_pipeline_desc) + ), + onShowHelp = onShowHelp + ) + val hasSupportedMemoryMapping = supportedMemoryMappingMask > 1 + val memoryMappingTitle = stringResource(R.string.settings_gpu_memory_mapping) + val memoryMappingHelp = helpEntry(memoryMappingTitle, stringResource(R.string.settings_gpu_memory_mapping_desc)) + val memoryMappingOptions = buildList { + add(stringResource(R.string.settings_value_disabled) to "disabled") + if ((supportedMemoryMappingMask and (1 shl 1)) != 0) add("Double Buffer" to "double-buffer") + if ((supportedMemoryMappingMask and (1 shl 2)) != 0) add("External Host" to "external-host") + if ((supportedMemoryMappingMask and (1 shl 3)) != 0) add("Page Table" to "page-table") + if ((supportedMemoryMappingMask and (1 shl 4)) != 0) add("Native Buffer" to "native-buffer") + } + val selectedIndex = memoryMappingOptions.indexOfFirst { it.second == cfg.memoryMapping }.takeIf { it >= 0 } ?: 0 + SettingsChoiceField( + title = memoryMappingTitle, + options = memoryMappingOptions.map { it.first }, + selectedIndex = if (hasSupportedMemoryMapping) selectedIndex else 0, + onSelect = { index -> onUpdate { memoryMapping = memoryMappingOptions[index].second } }, + enabled = hasSupportedMemoryMapping, + help = memoryMappingHelp, + onShowHelp = onShowHelp + ) + if (!hasSupportedMemoryMapping) { + SettingsNote(text = stringResource(R.string.settings_gpu_memory_mapping_unsupported)) + } + } + } + + if (isVulkan && showCustomDriverSection) { + SettingsSectionCard( + title = stringResource(R.string.settings_gpu_android_driver_section), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + val customDriverTitle = stringResource(R.string.settings_gpu_custom_driver) + val customDriverHelp = helpEntry(customDriverTitle, stringResource(R.string.settings_gpu_custom_driver_desc)) + val defaultCustomDriverLabel = stringResource(R.string.settings_gpu_custom_driver_default) + val selectedDriverMissing = cfg.customDriverName.isNotEmpty() && cfg.customDriverName !in installedCustomDrivers + val options = buildList { + add(defaultCustomDriverLabel) + addAll(installedCustomDrivers) + if (selectedDriverMissing) { + add(stringResource(R.string.settings_gpu_custom_driver_missing, cfg.customDriverName)) + } + } + val selectedIndex = when { + cfg.customDriverName.isEmpty() -> 0 + cfg.customDriverName in installedCustomDrivers -> installedCustomDrivers.indexOf(cfg.customDriverName) + 1 + selectedDriverMissing -> options.lastIndex + else -> 0 + } + + SettingsChoiceField( + title = customDriverTitle, + options = options, + selectedIndex = selectedIndex, + enabled = !customDriverBusy, + onSelect = { index -> + val selectedDriverName = when { + index == 0 -> "" + index - 1 < installedCustomDrivers.size -> installedCustomDrivers[index - 1] + else -> cfg.customDriverName + } + onSelectCustomDriver(selectedDriverName) + }, + help = customDriverHelp, + onShowHelp = onShowHelp + ) + + if (selectedDriverMissing) { + SettingsNote( + text = stringResource(R.string.settings_gpu_custom_driver_missing_desc), + color = MaterialTheme.colorScheme.error + ) + } else if (cfg.customDriverName.isNotEmpty()) { + val statusText = when (customDriverLoadStatus) { + CustomDriverLoadStatus.Loaded -> stringResource(R.string.settings_gpu_custom_driver_active_desc) + CustomDriverLoadStatus.Fallback -> stringResource(R.string.settings_gpu_custom_driver_fallback_desc) + CustomDriverLoadStatus.Default -> null + } + if (statusText != null) { + SettingsNote( + text = statusText, + color = if (customDriverLoadStatus == CustomDriverLoadStatus.Fallback) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + } + ) + } + } + + if (showCustomDriverManagement) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton(onClick = onDownloadCustomDriver, enabled = !customDriverBusy) { + Text(stringResource(R.string.settings_gpu_download_custom_driver)) + } + FilledTonalButton(onClick = onInstallCustomDriver, enabled = !customDriverBusy) { + Text(stringResource(R.string.settings_gpu_install_custom_driver)) + } + if (cfg.customDriverName.isNotEmpty() && cfg.customDriverName in installedCustomDrivers) { + OutlinedButton( + onClick = { onRequestRemoveCustomDriver(cfg.customDriverName) }, + enabled = !customDriverBusy + ) { + Text(stringResource(R.string.settings_gpu_remove_custom_driver)) + } + } + if (customDriverBusy) { + CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp) + } + } + } + } + } + + SettingsSectionCard( + title = stringResource(R.string.settings_gpu_image_quality), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + val sliderValue = (cfg.resolutionMultiplier * 4f).roundToInt().coerceIn(2, 32).toFloat() + val multiplier = sliderValue / 4f + SettingsSliderRow( + title = stringResource(R.string.settings_gpu_resolution), + valueLabel = stringResource( + R.string.settings_gpu_resolution_val, + (960 * multiplier).toInt(), + (544 * multiplier).toInt(), + String.format("%.2f", multiplier) + ), + value = sliderValue, + onValueChange = { newValue -> + onUpdate { resolutionMultiplier = newValue.roundToInt().coerceIn(2, 32) / 4f } + }, + valueRange = 2f..32f, + steps = 29, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_resolution), + body = stringResource(R.string.settings_gpu_resolution_desc) + ), + onShowHelp = onShowHelp + ) + val anisoOptions = listOf("1x", "2x", "4x", "8x", "16x") + val anisoValues = listOf(1, 2, 4, 8, 16) + val anisotropicTitle = stringResource(R.string.settings_gpu_anisotropic) + val anisotropicHelp = helpEntry(anisotropicTitle, stringResource(R.string.settings_gpu_anisotropic_desc)) + SettingsChoiceField( + title = anisotropicTitle, + options = anisoOptions, + selectedIndex = anisoValues.indexOfFirst { it >= cfg.anisotropicFiltering }.coerceAtLeast(0), + onSelect = { index -> onUpdate { anisotropicFiltering = anisoValues[index] } }, + help = anisotropicHelp, + onShowHelp = onShowHelp + ) + } + + SettingsSectionCard( + title = stringResource(R.string.settings_gpu_textures_shaders), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + SettingsSubsectionTitle(title = stringResource(R.string.settings_gpu_textures)) + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_export_textures), + checked = cfg.exportTextures, + onCheckedChange = { onUpdate { exportTextures = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_export_textures), + body = stringResource(R.string.settings_gpu_export_textures_desc) + ), + onShowHelp = onShowHelp + ) + if (cfg.exportTextures) { + val textureFormatTitle = stringResource(R.string.settings_gpu_texture_format) + val textureFormatOptions = listOf("PNG", "DDS") + SettingsChoiceField( + title = textureFormatTitle, + options = textureFormatOptions, + selectedIndex = if (cfg.exportAsPng) 0 else 1, + onSelect = { index -> onUpdate { exportAsPng = index == 0 } }, + help = SettingsHelpEntry( + title = textureFormatTitle, + body = stringResource(R.string.settings_gpu_texture_format_desc) + ), + onShowHelp = onShowHelp + ) + } + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_import_textures), + checked = cfg.importTextures, + onCheckedChange = { onUpdate { importTextures = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_import_textures), + body = stringResource(R.string.settings_gpu_import_textures_desc) + ), + onShowHelp = onShowHelp + ) + SettingsSubsectionTitle(title = stringResource(R.string.settings_gpu_shaders)) + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_shader_cache), + checked = cfg.shaderCache, + onCheckedChange = { onUpdate { shaderCache = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_shader_cache), + body = stringResource(R.string.settings_gpu_shader_cache_desc) + ), + onShowHelp = onShowHelp + ) + if (isVulkan) { + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_spirv_shader), + checked = cfg.spirvShader, + onCheckedChange = { onUpdate { spirvShader = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_spirv_shader), + body = stringResource(R.string.settings_gpu_spirv_shader_desc) + ), + onShowHelp = onShowHelp + ) + } + SettingsSubsectionTitle(title = stringResource(R.string.settings_gpu_hacks)) + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_fps_hack), + checked = cfg.fpsHack, + onCheckedChange = { onUpdate { fpsHack = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_fps_hack), + body = stringResource(R.string.settings_gpu_fps_hack_desc) + ), + onShowHelp = onShowHelp + ) + if (!isPerApp && isVulkan && showTurboModeOption) { + SettingsToggleRow( + title = stringResource(R.string.settings_gpu_turbo_mode), + checked = cfg.turboMode, + onCheckedChange = { onUpdate { turboMode = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_gpu_turbo_mode), + body = stringResource(R.string.settings_gpu_turbo_mode_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } + } + } + +@Composable +private fun CameraSettingsSection( + cfg: EmulatorConfig, + availableCameras: List, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit, + onPickCameraImage: (Boolean) -> Unit, + allowImagePicker: Boolean +) { + val cameraSections = listOf( + CameraSectionState( + isFront = true, + title = stringResource(R.string.settings_camera_front), + description = stringResource(R.string.settings_camera_front_desc), + sourceType = cfg.frontCameraType, + sourceId = cfg.frontCameraId, + imagePath = cfg.frontCameraImage, + colorValue = cfg.frontCameraColor + ), + CameraSectionState( + isFront = false, + title = stringResource(R.string.settings_camera_back), + description = stringResource(R.string.settings_camera_back_desc), + sourceType = cfg.backCameraType, + sourceId = cfg.backCameraId, + imagePath = cfg.backCameraImage, + colorValue = cfg.backCameraColor + ) + ) + + cameraSections.forEach { section -> + CameraDeviceSection( + title = section.title, + description = section.description, + sourceType = section.sourceType, + sourceId = section.sourceId, + imagePath = section.imagePath, + colorValue = section.colorValue, + availableCameras = availableCameras, + onSourceSelected = { index -> + onUpdate { + when { + index == CAMERA_SOURCE_SOLID -> { + if (section.isFront) { + frontCameraType = CAMERA_SOURCE_SOLID + frontCameraColor = normalizedCameraColor(frontCameraColor) + } else { + backCameraType = CAMERA_SOURCE_SOLID + backCameraColor = normalizedCameraColor(backCameraColor) + } + } + + index == CAMERA_SOURCE_IMAGE -> { + if (section.isFront) { + frontCameraType = CAMERA_SOURCE_IMAGE + } else { + backCameraType = CAMERA_SOURCE_IMAGE + } + } + + index - CAMERA_SOURCE_DEVICE in availableCameras.indices -> { + val cameraId = availableCameras[index - CAMERA_SOURCE_DEVICE] + if (section.isFront) { + frontCameraType = CAMERA_SOURCE_DEVICE + frontCameraId = cameraId + } else { + backCameraType = CAMERA_SOURCE_DEVICE + backCameraId = cameraId + } + } + } + } + }, + onColorChanged = { updated -> + onUpdate { + if (section.isFront) { + frontCameraColor = normalizedCameraColor(updated) + } else { + backCameraColor = normalizedCameraColor(updated) + } + } + }, + onPickImage = { onPickCameraImage(section.isFront) }, + allowImagePicker = allowImagePicker, + onShowHelp = onShowHelp + ) + } +} + +@Composable +private fun CameraDeviceSection( + title: String, + description: String, + sourceType: Int, + sourceId: String, + imagePath: String, + colorValue: Long, + availableCameras: List, + onSourceSelected: (Int) -> Unit, + onColorChanged: (Long) -> Unit, + onPickImage: () -> Unit, + allowImagePicker: Boolean, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val sourceTitle = stringResource(R.string.settings_camera_source) + val sourceHelp = helpEntry(title, description, SettingsScope.Global) + val imageTitle = stringResource(R.string.settings_camera_image) + val imageHelp = helpEntry(imageTitle, stringResource(R.string.settings_camera_image_desc), SettingsScope.Global) + val solidLabel = stringResource(R.string.settings_camera_solid_color) + val staticLabel = stringResource(R.string.settings_camera_static_image) + val noDeviceLabel = stringResource(R.string.settings_camera_no_devices) + val noImageLabel = stringResource(R.string.settings_camera_no_image) + val sourceOptions = buildList { + add(solidLabel) + add(staticLabel) + if (availableCameras.isEmpty()) { + add(noDeviceLabel) + } else { + addAll(availableCameras) + } + } + val displaySourceType = if (sourceType in CAMERA_SOURCE_SOLID..CAMERA_SOURCE_IMAGE || availableCameras.isNotEmpty()) { + sourceType + } else { + CAMERA_SOURCE_DEVICE + } + val sourceValue = cameraSourceValue(displaySourceType, sourceId, availableCameras, solidLabel, staticLabel) + + SettingsSectionCard( + title = title, + summary = sourceValue, + help = sourceHelp, + onShowHelp = onShowHelp + ) { + SettingsChoiceSelector( + title = sourceTitle, + options = sourceOptions, + selectedIndex = cameraSourceIndex(displaySourceType, sourceId, availableCameras), + onSelected = onSourceSelected, + summary = sourceValue, + onShowHelp = onShowHelp + ) + + when (displaySourceType) { + CAMERA_SOURCE_SOLID -> CameraColorControls(colorValue = colorValue, onColorChanged = onColorChanged) + CAMERA_SOURCE_IMAGE -> SettingsActionRow( + title = imageTitle, + value = cameraImageValue(imagePath, noImageLabel), + onClick = onPickImage, + enabled = allowImagePicker, + help = imageHelp, + onShowHelp = onShowHelp + ) + else -> Unit + } + } +} + +@Composable +private fun CameraColorControls( + colorValue: Long, + onColorChanged: (Long) -> Unit +) { + val normalizedColor = normalizedCameraColor(colorValue) + var draftColor by remember(normalizedColor) { mutableLongStateOf(normalizedColor) } + + val red = cameraColorChannel(draftColor, 16) + val green = cameraColorChannel(draftColor, 8) + val blue = cameraColorChannel(draftColor, 0) + val previewColor = cameraColor(draftColor) + + fun commitDraftColor() { + val committedColor = normalizedCameraColor(draftColor) + if (committedColor != normalizedColor) { + onColorChanged(committedColor) + } + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp, bottom = 4.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f), + shape = RoundedCornerShape(18.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp) + ) { + Surface( + modifier = Modifier.size(54.dp), + shape = RoundedCornerShape(16.dp), + color = previewColor + ) { + Box(modifier = Modifier.fillMaxWidth()) + } + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = stringResource(R.string.settings_camera_color), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = cameraColorHex(draftColor), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + CameraColorSlider( + label = "R", + value = red, + color = Color(0xFFE57373), + onValueChange = { updated -> + if (updated != red) { + draftColor = cameraColorValue(updated, green, blue) + } + }, + onValueChangeFinished = ::commitDraftColor + ) + CameraColorSlider( + label = "G", + value = green, + color = Color(0xFF81C784), + onValueChange = { updated -> + if (updated != green) { + draftColor = cameraColorValue(red, updated, blue) + } + }, + onValueChangeFinished = ::commitDraftColor + ) + CameraColorSlider( + label = "B", + value = blue, + color = Color(0xFF64B5F6), + onValueChange = { updated -> + if (updated != blue) { + draftColor = cameraColorValue(red, green, updated) + } + }, + onValueChangeFinished = ::commitDraftColor + ) +} + +@Composable +private fun CameraColorSlider( + label: String, + value: Int, + color: Color, + onValueChange: (Int) -> Unit, + onValueChangeFinished: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.24f), + shape = RoundedCornerShape(18.dp) + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Surface( + modifier = Modifier.size(14.dp), + shape = RoundedCornerShape(999.dp), + color = color + ) { + Box(modifier = Modifier.fillMaxWidth()) + } + Text( + text = label, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = value.toString(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + androidx.compose.material3.Slider( + value = value.toFloat(), + onValueChange = { onValueChange(it.roundToInt().coerceIn(0, 255)) }, + onValueChangeFinished = onValueChangeFinished, + valueRange = 0f..255f, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + } + } +} + +@Composable +private fun AudioSettingsSection( + cfg: EmulatorConfig, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsSectionCard(title = stringResource(R.string.settings_audio_output), summary = null, help = null, onShowHelp = onShowHelp) { + val audioBackendTitle = stringResource(R.string.settings_audio_backend) + val audioBackendHelp = helpEntry(audioBackendTitle, stringResource(R.string.settings_audio_backend_desc)) + val backendOptions = listOf("SDL", "Cubeb") + SettingsChoiceField( + title = audioBackendTitle, + options = backendOptions, + selectedIndex = backendOptions.indexOf(cfg.audioBackend).coerceAtLeast(0), + onSelect = { index -> onUpdate { audioBackend = backendOptions[index] } }, + help = audioBackendHelp, + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_audio_volume), + valueLabel = stringResource(R.string.settings_audio_volume_value, cfg.audioVolume), + value = cfg.audioVolume.toFloat().coerceIn(0f, 100f), + onValueChange = { onUpdate { audioVolume = it.roundToInt().coerceIn(0, 100) } }, + valueRange = 0f..100f, + steps = 0, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_audio_volume), + body = stringResource(R.string.settings_audio_volume_desc) + ), + onShowHelp = onShowHelp + ) + SettingsSubsectionTitle(title = stringResource(R.string.settings_audio_features)) + SettingsToggleRow( + title = stringResource(R.string.settings_audio_ngs), + checked = cfg.ngsEnable, + onCheckedChange = { onUpdate { ngsEnable = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_audio_ngs), + body = stringResource(R.string.settings_audio_ngs_desc) + ), + onShowHelp = onShowHelp + ) + } +} + +@Composable +private fun SystemSettingsSection( + cfg: EmulatorConfig, + isPerApp: Boolean, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsSectionCard(title = stringResource(R.string.settings_system_console_settings), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsToggleRow( + title = stringResource(R.string.settings_system_pstv_mode), + checked = cfg.pstvMode, + onCheckedChange = { onUpdate { pstvMode = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_system_pstv_mode), + body = stringResource(R.string.settings_system_pstv_mode_desc) + ), + onShowHelp = onShowHelp + ) + if (!isPerApp) { + SettingsToggleRow( + title = stringResource(R.string.settings_system_show_mode), + checked = cfg.showMode, + onCheckedChange = { onUpdate { showMode = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_system_show_mode), + body = stringResource(R.string.settings_system_show_mode_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_system_demo_mode), + checked = cfg.demoMode, + onCheckedChange = { onUpdate { demoMode = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_system_demo_mode), + body = stringResource(R.string.settings_system_demo_mode_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } + val enterButtonTitle = stringResource(R.string.settings_system_enter_button) + val enterButtonOptions = listOf( + stringResource(R.string.settings_system_enter_circle), + stringResource(R.string.settings_system_enter_cross) + ) + val enterButtonHelp = helpEntry(enterButtonTitle, stringResource(R.string.settings_system_enter_button_desc)) + SettingsChoiceField( + title = enterButtonTitle, + options = enterButtonOptions, + selectedIndex = if (cfg.sysButton == 0) 0 else 1, + onSelect = { index -> onUpdate { sysButton = if (index == 0) 0 else 1 } }, + help = enterButtonHelp, + onShowHelp = onShowHelp + ) + } + + SettingsSectionCard(title = stringResource(R.string.settings_system_region_language), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsSubsectionTitle(title = stringResource(R.string.settings_system_system_settings_section)) + val systemLanguageTitle = stringResource(R.string.settings_system_language) + val systemLanguageOptions = FirmwareLinks.locales.map { stringResource(it.nameResId) } + val systemLanguageHelp = helpEntry(systemLanguageTitle, stringResource(R.string.settings_system_language_desc)) + SettingsScrollableChoiceField( + title = systemLanguageTitle, + options = systemLanguageOptions, + selectedIndex = cfg.sysLang.coerceIn(0, systemLanguageOptions.lastIndex), + onSelect = { index -> onUpdate { sysLang = index } }, + help = systemLanguageHelp, + onShowHelp = onShowHelp + ) + val dateFormatTitle = stringResource(R.string.settings_system_date_format) + val dateFormatOptions = listOf( + stringResource(R.string.settings_opt_date_ymd), + stringResource(R.string.settings_opt_date_dmy), + stringResource(R.string.settings_opt_date_mdy) + ) + val dateFormatHelp = helpEntry(dateFormatTitle, stringResource(R.string.settings_system_date_format_desc)) + SettingsChoiceField( + title = dateFormatTitle, + options = dateFormatOptions, + selectedIndex = cfg.sysDateFormat.coerceIn(0, 2), + onSelect = { index -> onUpdate { sysDateFormat = index } }, + help = dateFormatHelp, + onShowHelp = onShowHelp + ) + val timeFormatTitle = stringResource(R.string.settings_system_time_format) + val timeFormatOptions = listOf( + stringResource(R.string.settings_opt_12h), + stringResource(R.string.settings_opt_24h) + ) + val timeFormatHelp = helpEntry(timeFormatTitle, stringResource(R.string.settings_system_time_format_desc)) + SettingsChoiceField( + title = timeFormatTitle, + options = timeFormatOptions, + selectedIndex = cfg.sysTimeFormat.coerceIn(0, 1), + onSelect = { index -> onUpdate { sysTimeFormat = index } }, + help = timeFormatHelp, + onShowHelp = onShowHelp + ) + } + + SettingsSectionCard( + title = stringResource(R.string.settings_system_ime_langs), + summary = stringResource(R.string.settings_system_ime_langs_desc), + help = SettingsHelpEntry( + title = stringResource(R.string.settings_system_ime_langs), + body = stringResource(R.string.settings_system_ime_langs_desc) + ), + onShowHelp = onShowHelp + ) { + ImeLanguageSelector(cfg = cfg, onUpdate = onUpdate) + } +} + +@Composable +private fun NetworkSettingsSection( + cfg: EmulatorConfig, + isPerApp: Boolean, + availableAdhocAddresses: List>, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsSectionCard(title = stringResource(R.string.settings_network_connection), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsToggleRow( + title = stringResource(R.string.settings_network_psn_signed_in), + checked = cfg.psnSignedIn, + onCheckedChange = { onUpdate { psnSignedIn = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_network_psn_signed_in), + body = stringResource(R.string.settings_network_psn_signed_in_desc), + scope = SettingsScope.PerApp + ), + onShowHelp = onShowHelp + ) + if (!isPerApp && availableAdhocAddresses.isNotEmpty()) { + val adhocTitle = stringResource(R.string.settings_network_adhoc_address) + val adhocHelp = helpEntry(adhocTitle, stringResource(R.string.settings_network_adhoc_address_desc), SettingsScope.Global) + val adhocOptions = availableAdhocAddresses.map { it.first } + val selectedIndex = cfg.adhocAddr.coerceIn(0, adhocOptions.lastIndex) + SettingsChoiceField( + title = adhocTitle, + options = adhocOptions, + selectedIndex = selectedIndex, + onSelect = { index -> onUpdate { adhocAddr = index } }, + help = adhocHelp, + onShowHelp = onShowHelp + ) + SettingsNote( + text = stringResource( + R.string.settings_network_subnet_mask_value, + availableAdhocAddresses.getOrElse(selectedIndex) { availableAdhocAddresses.first() }.second + ) + ) + } + } + + if (!isPerApp) { + SettingsSectionCard(title = stringResource(R.string.settings_network_http_networking), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsSubsectionTitle(title = stringResource(R.string.settings_network_timeout_retry)) + SettingsToggleRow( + title = stringResource(R.string.settings_network_http_enable), + checked = cfg.httpEnable, + onCheckedChange = { onUpdate { httpEnable = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_network_http_enable), + body = stringResource(R.string.settings_network_http_enable_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_network_http_timeout_attempts), + valueLabel = stringResource(R.string.settings_network_http_attempts_value, cfg.httpTimeoutAttempts), + value = cfg.httpTimeoutAttempts.toFloat().coerceIn(0f, 100f), + onValueChange = { onUpdate { httpTimeoutAttempts = it.roundToInt().coerceIn(0, 100) } }, + valueRange = 0f..100f, + steps = 99, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_network_http_timeout_attempts), + body = stringResource(R.string.settings_network_http_timeout_attempts_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_network_http_timeout_sleep), + valueLabel = stringResource(R.string.settings_network_http_sleep_value, cfg.httpTimeoutSleepMs), + value = cfg.httpTimeoutSleepMs.toFloat().coerceIn(50f, 3000f), + onValueChange = { onUpdate { httpTimeoutSleepMs = (it.roundToInt().coerceIn(50, 3000) / 50) * 50 } }, + valueRange = 50f..3000f, + steps = 58, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_network_http_timeout_sleep), + body = stringResource(R.string.settings_network_http_timeout_sleep_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_network_http_read_end_attempts), + valueLabel = stringResource(R.string.settings_network_http_attempts_value, cfg.httpReadEndAttempts), + value = cfg.httpReadEndAttempts.toFloat().coerceIn(0f, 100f), + onValueChange = { onUpdate { httpReadEndAttempts = it.roundToInt().coerceIn(0, 100) } }, + valueRange = 0f..100f, + steps = 99, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_network_http_read_end_attempts), + body = stringResource(R.string.settings_network_http_read_end_attempts_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_network_http_read_end_sleep), + valueLabel = stringResource(R.string.settings_network_http_sleep_value, cfg.httpReadEndSleepMs), + value = cfg.httpReadEndSleepMs.toFloat().coerceIn(50f, 3000f), + onValueChange = { onUpdate { httpReadEndSleepMs = (it.roundToInt().coerceIn(50, 3000) / 50) * 50 } }, + valueRange = 50f..3000f, + steps = 58, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_network_http_read_end_sleep), + body = stringResource(R.string.settings_network_http_read_end_sleep_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } + } +} + +@Composable +private fun DebugSettingsSection( + cfg: EmulatorConfig, + isPerApp: Boolean, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + val isVulkan = isVulkanBackend(cfg.backendRenderer) + SettingsSectionCard(title = stringResource(R.string.settings_debug_diagnostics), summary = null, help = null, onShowHelp = onShowHelp) { + if (!isPerApp) { + SettingsToggleRow( + title = stringResource(R.string.settings_debug_log_imports), + checked = cfg.logImports, + onCheckedChange = { onUpdate { logImports = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_debug_log_imports), + body = stringResource(R.string.settings_debug_log_imports_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_debug_log_exports), + checked = cfg.logExports, + onCheckedChange = { onUpdate { logExports = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_debug_log_exports), + body = stringResource(R.string.settings_debug_log_exports_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } + SettingsToggleRow( + title = stringResource(R.string.settings_debug_log_active_shaders), + checked = cfg.logActiveShaders, + onCheckedChange = { onUpdate { logActiveShaders = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_debug_log_active_shaders), + body = stringResource(R.string.settings_debug_log_active_shaders_desc) + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_debug_log_uniforms), + checked = cfg.logUniforms, + onCheckedChange = { onUpdate { logUniforms = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_debug_log_uniforms), + body = stringResource(R.string.settings_debug_log_uniforms_desc) + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_debug_color_surface), + checked = cfg.colorSurfaceDebug, + onCheckedChange = { onUpdate { colorSurfaceDebug = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_debug_color_surface), + body = stringResource(R.string.settings_debug_color_surface_desc) + ), + onShowHelp = onShowHelp + ) + if (isVulkan) { + SettingsToggleRow( + title = stringResource(R.string.settings_debug_validation_layer), + checked = cfg.validationLayer, + onCheckedChange = { onUpdate { validationLayer = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_debug_validation_layer), + body = stringResource(R.string.settings_debug_validation_layer_desc) + ), + onShowHelp = onShowHelp + ) + } + if (!isPerApp) { + SettingsToggleRow( + title = stringResource(R.string.settings_debug_dump_elfs), + checked = cfg.dumpElfs, + onCheckedChange = { onUpdate { dumpElfs = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_debug_dump_elfs), + body = stringResource(R.string.settings_debug_dump_elfs_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } + } +} + +@Composable +private fun InterfaceSettingsSection( + cfg: EmulatorConfig, + isPerApp: Boolean, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + if (isPerApp) { + return + } + + SettingsSectionCard( + title = stringResource(R.string.settings_interface_appearance), + summary = null, + help = null, + onShowHelp = onShowHelp + ) { + SettingsSubsectionTitle(title = stringResource(R.string.settings_interface_ui_options)) + val uiLanguageTitle = stringResource(R.string.settings_emulator_ui_language) + val uiLanguageOptions = UiLanguages.options + val uiLanguageIndex = uiLanguageOptions.indexOfFirst { it.tag == cfg.userLang }.let { index -> + if (index >= 0) index else 0 + } + SettingsScrollableChoiceField( + title = uiLanguageTitle, + options = uiLanguageOptions.map { it.label }, + selectedIndex = uiLanguageIndex, + onSelect = { index -> onUpdate { userLang = uiLanguageOptions[index].tag } }, + help = SettingsHelpEntry( + title = uiLanguageTitle, + body = stringResource(R.string.settings_emulator_ui_language_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } +} + +@Composable +private fun EmulatorSettingsSection( + cfg: EmulatorConfig, + isPerApp: Boolean, + currentStoragePath: String, + defaultStoragePath: String, + onChangeStorageFolder: () -> Unit, + onResetStorageFolder: () -> Unit, + allowStorageFolderManagement: Boolean, + allowClearAllCustomConfigs: Boolean, + onClearAllCustomConfigs: () -> Unit, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit, + onShowHelp: (SettingsHelpEntry) -> Unit +) { + SettingsSectionCard(title = stringResource(R.string.settings_emulator_behavior), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsToggleRow( + title = stringResource(R.string.settings_emulator_texture_cache), + checked = cfg.textureCache, + onCheckedChange = { onUpdate { textureCache = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_texture_cache), + body = stringResource(R.string.settings_emulator_texture_cache_desc) + ), + onShowHelp = onShowHelp + ) + SettingsSliderRow( + title = stringResource(R.string.settings_emulator_file_loading_delay, cfg.fileLoadingDelay), + valueLabel = "${cfg.fileLoadingDelay} ms", + value = cfg.fileLoadingDelay.toFloat().coerceIn(0f, 30f), + onValueChange = { onUpdate { fileLoadingDelay = it.roundToInt().coerceIn(0, 30) } }, + valueRange = 0f..30f, + steps = 29, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_file_loading_delay, cfg.fileLoadingDelay), + body = stringResource(R.string.settings_emulator_file_loading_delay_desc) + ), + onShowHelp = onShowHelp + ) + if (!isPerApp && allowStorageFolderManagement) { + val storageTitle = stringResource(R.string.settings_emulator_storage_folder) + SettingsActionRow( + title = storageTitle, + value = currentStoragePath, + onClick = onChangeStorageFolder, + actionLabel = stringResource(R.string.action_reset), + onActionClick = onResetStorageFolder, + actionEnabled = currentStoragePath != defaultStoragePath, + help = SettingsHelpEntry( + title = storageTitle, + body = stringResource(R.string.settings_emulator_storage_folder_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_emulator_show_compile_shaders), + checked = cfg.showCompileShaders, + onCheckedChange = { onUpdate { showCompileShaders = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_show_compile_shaders), + body = stringResource(R.string.settings_emulator_show_compile_shaders_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + val updateModeTitle = stringResource(R.string.settings_emulator_check_updates) + SettingsChoiceField( + title = updateModeTitle, + options = listOf( + stringResource(R.string.settings_emulator_check_updates_check), + stringResource(R.string.settings_emulator_check_updates_dont_check) + ), + selectedIndex = if (cfg.checkForUpdatesMode == 0) 1 else 0, + onSelect = { index -> + onUpdate { + checkForUpdatesMode = if (index == 0) 1 else 0 + checkForUpdates = checkForUpdatesMode != 0 + } + }, + help = SettingsHelpEntry( + title = updateModeTitle, + body = stringResource(R.string.settings_emulator_check_updates_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_emulator_archive_log), + checked = cfg.archiveLog, + onCheckedChange = { onUpdate { archiveLog = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_archive_log), + body = stringResource(R.string.settings_emulator_archive_log_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_emulator_log_compat_warn), + checked = cfg.logCompatWarn, + onCheckedChange = { onUpdate { logCompatWarn = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_log_compat_warn), + body = stringResource(R.string.settings_emulator_log_compat_warn_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + val logOptions = listOf( + stringResource(R.string.settings_opt_trace), + stringResource(R.string.settings_opt_debug), + stringResource(R.string.settings_opt_info), + stringResource(R.string.settings_opt_warning), + stringResource(R.string.settings_opt_error), + stringResource(R.string.settings_opt_critical), + stringResource(R.string.settings_opt_off) + ) + val logLevelTitle = stringResource(R.string.settings_emulator_log_level) + val logLevelHelp = helpEntry( + logLevelTitle, + stringResource(R.string.settings_emulator_log_level_desc), + SettingsScope.Global + ) + SettingsChoiceField( + title = logLevelTitle, + options = logOptions, + selectedIndex = cfg.logLevel.coerceIn(0, logOptions.lastIndex), + onSelect = { index -> onUpdate { logLevel = index } }, + help = logLevelHelp, + onShowHelp = onShowHelp + ) + val screenshotOptions = listOf(stringResource(R.string.settings_opt_none), "JPEG", "PNG") + val screenshotFormatTitle = stringResource(R.string.settings_emulator_screenshot_format) + val screenshotFormatHelp = helpEntry( + screenshotFormatTitle, + stringResource(R.string.settings_emulator_screenshot_format_desc), + SettingsScope.Global + ) + SettingsChoiceField( + title = screenshotFormatTitle, + options = screenshotOptions, + selectedIndex = cfg.screenshotFormat.coerceIn(0, screenshotOptions.lastIndex), + onSelect = { index -> onUpdate { screenshotFormat = index } }, + help = screenshotFormatHelp, + onShowHelp = onShowHelp + ) + } + } + + if (!isPerApp && allowClearAllCustomConfigs) { + SettingsSectionCard(title = stringResource(R.string.settings_clear_all_custom_configs), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsActionRow( + title = stringResource(R.string.settings_clear_all_custom_configs), + value = null, + onClick = onClearAllCustomConfigs, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_clear_all_custom_configs), + body = stringResource(R.string.settings_confirm_clear_all_message), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + } + } + + SettingsSectionCard(title = stringResource(R.string.settings_emulator_display_overlay), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsToggleRow( + title = stringResource(R.string.settings_emulator_stretch_display), + checked = cfg.stretchDisplayArea, + onCheckedChange = { onUpdate { stretchDisplayArea = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_stretch_display), + body = stringResource(R.string.settings_emulator_stretch_display_desc) + ), + onShowHelp = onShowHelp + ) + SettingsToggleRow( + title = stringResource(R.string.settings_emulator_pixel_perfect), + checked = cfg.fullscreenHdResPixelPerfect, + onCheckedChange = { onUpdate { fullscreenHdResPixelPerfect = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_pixel_perfect), + body = stringResource(R.string.settings_emulator_pixel_perfect_desc) + ), + onShowHelp = onShowHelp + ) + } + + if (!isPerApp) { + SettingsSectionCard(title = stringResource(R.string.settings_emulator_perf_overlay), summary = null, help = null, onShowHelp = onShowHelp) { + SettingsToggleRow( + title = stringResource(R.string.settings_emulator_perf_overlay), + checked = cfg.performanceOverlay, + onCheckedChange = { onUpdate { performanceOverlay = it } }, + help = SettingsHelpEntry( + title = stringResource(R.string.settings_emulator_perf_overlay), + body = stringResource(R.string.settings_emulator_perf_overlay_desc), + scope = SettingsScope.Global + ), + onShowHelp = onShowHelp + ) + if (cfg.performanceOverlay) { + val detailOptions = listOf( + stringResource(R.string.settings_opt_minimum), + stringResource(R.string.settings_opt_low), + stringResource(R.string.settings_opt_medium), + stringResource(R.string.settings_opt_maximum) + ) + val perfOverlayDetailTitle = stringResource(R.string.settings_emulator_perf_overlay_detail) + val perfOverlayDetailHelp = helpEntry( + perfOverlayDetailTitle, + stringResource(R.string.settings_emulator_perf_overlay_detail_desc), + SettingsScope.Global + ) + SettingsChoiceField( + title = perfOverlayDetailTitle, + options = detailOptions, + selectedIndex = cfg.performanceOverlayDetail.coerceIn(0, detailOptions.lastIndex), + onSelect = { index -> onUpdate { performanceOverlayDetail = index } }, + help = perfOverlayDetailHelp, + onShowHelp = onShowHelp + ) + val positionOptions = listOf( + stringResource(R.string.settings_opt_top_left), + stringResource(R.string.settings_opt_top_center), + stringResource(R.string.settings_opt_top_right), + stringResource(R.string.settings_opt_bottom_left), + stringResource(R.string.settings_opt_bottom_center), + stringResource(R.string.settings_opt_bottom_right) + ) + val perfOverlayPositionTitle = stringResource(R.string.settings_emulator_perf_overlay_position) + val perfOverlayPositionHelp = helpEntry( + perfOverlayPositionTitle, + stringResource(R.string.settings_emulator_perf_overlay_position_desc), + SettingsScope.Global + ) + SettingsChoiceField( + title = perfOverlayPositionTitle, + options = positionOptions, + selectedIndex = cfg.performanceOverlayPosition.coerceIn(0, positionOptions.lastIndex), + onSelect = { index -> onUpdate { performanceOverlayPosition = index } }, + help = perfOverlayPositionHelp, + onShowHelp = onShowHelp + ) + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ImeLanguageSelector( + cfg: EmulatorConfig, + onUpdate: (EmulatorConfig.() -> Unit) -> Unit +) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + IME_LANGS.forEach { entry -> + val checked = (cfg.imeLangs and entry.flag) != 0L + FilterChip( + selected = checked, + onClick = { + onUpdate { + imeLangs = if (checked) imeLangs and entry.flag.inv() else imeLangs or entry.flag + } + }, + colors = settingsFilterChipColors(), + label = { Text(stringResource(entry.nameResId)) } + ) + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/theme/Theme.kt b/android/app/src/main/java/org/vita3k/emulator/ui/theme/Theme.kt new file mode 100644 index 000000000..5cd170287 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/theme/Theme.kt @@ -0,0 +1,97 @@ +package org.vita3k.emulator.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.window.DialogWindowProvider +import androidx.core.view.WindowInsetsControllerCompat + +private val AppBackgroundColor = Color(0xFF111111) +private val AppTopBarColor = Color(0xFF1C1C1C) + +private val AppColorScheme = darkColorScheme( + primary = Color(0xFFFF9800), + onPrimary = Color(0xFF1A0D00), + primaryContainer = Color(0xFF3D1F00), + onPrimaryContainer = Color(0xFFFFD8A8), + secondary = Color(0xFF9E9E9E), + onSecondary = Color(0xFF1A1A1A), + secondaryContainer = Color(0xFF2C2C2C), + onSecondaryContainer = Color(0xFFE0E0E0), + tertiary = Color(0xFFBDBDBD), + onTertiary = Color(0xFF1A1A1A), + background = AppBackgroundColor, + onBackground = Color(0xFFE8E8E8), + surface = AppTopBarColor, + onSurface = Color(0xFFE8E8E8), + surfaceVariant = Color(0xFF2A2A2A), + onSurfaceVariant = Color(0xFFB0B0B0), + surfaceContainerHigh = Color(0xFF222222), + surfaceContainerHighest = Color(0xFF2A2A2A), + outline = Color(0xFF444444), + error = Color(0xFFFF6B6B), + onError = Color(0xFF1A0000) +) + +@Composable +@Suppress("DEPRECATION") +fun Vita3KTheme( + content: @Composable () -> Unit +) { + val view = LocalView.current + if (!view.isInEditMode) { + val transparentSystemBar = Color.Transparent.toArgb() + SideEffect { + val window = (view.context as? Activity)?.window ?: return@SideEffect + window.statusBarColor = transparentSystemBar + window.navigationBarColor = transparentSystemBar + WindowInsetsControllerCompat(window, view).apply { + isAppearanceLightStatusBars = false + isAppearanceLightNavigationBars = false + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isStatusBarContrastEnforced = false + window.isNavigationBarContrastEnforced = false + } + } + } + + MaterialTheme( + colorScheme = AppColorScheme, + content = content + ) +} + +/** + * A fraction in [0, 1] that is used uniformly for all modal scrim / dialog dim effects. + * Apply it to: + * - ModalBottomSheet via `scrimColor = Color.Black.copy(alpha = SCRIM_ALPHA)` + * - Dialog / AlertDialog via [ApplyDialogDim] + */ +const val SCRIM_ALPHA = 0.4f + +/** + * Must be called inside a Dialog or AlertDialog composable scope to reduce the system dim amount + * to the app-wide [SCRIM_ALPHA] value. Place it at the beginning of the dialog's content lambda. + */ +@Composable +fun ApplyDialogDim() { + val view = LocalView.current + if (!view.isInEditMode) { + // Check both the view itself and its parent — Compose's Dialog sets DialogWindowProvider + // on the DialogLayout (the direct parent of the composable view tree), but the exact + // position in the hierarchy can vary by API level and Material3 version. + DisposableEffect(Unit) { + val provider = (view as? DialogWindowProvider) + ?: (view.parent as? DialogWindowProvider) + provider?.window?.setDimAmount(SCRIM_ALPHA) + onDispose {} + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppAction.kt b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppAction.kt new file mode 100644 index 000000000..cc755247a --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppAction.kt @@ -0,0 +1,39 @@ +package org.vita3k.emulator.ui.viewmodel + +import androidx.annotation.StringRes +import org.vita3k.emulator.R + +enum class AppActionGroup( + @StringRes val labelResId: Int +) { + DELETE(R.string.action_group_delete), + OTHER(R.string.action_group_other) +} + +enum class AppAction( + @StringRes val labelResId: Int, + val destructive: Boolean, + val group: AppActionGroup, + val maskBit: Int +) { + DELETE_APPLICATION(R.string.app_action_application, true, AppActionGroup.DELETE, 1 shl 0), + DELETE_SAVE_DATA(R.string.app_action_save_data, true, AppActionGroup.DELETE, 1 shl 1), + DELETE_PATCH(R.string.app_action_patch, true, AppActionGroup.DELETE, 1 shl 2), + DELETE_DLC(R.string.app_action_dlc, true, AppActionGroup.DELETE, 1 shl 3), + DELETE_LICENSE(R.string.app_action_license, true, AppActionGroup.DELETE, 1 shl 4), + DELETE_SHADER_CACHE(R.string.app_action_shader_cache, true, AppActionGroup.DELETE, 1 shl 5), + DELETE_SHADER_LOG(R.string.app_action_shader_log, true, AppActionGroup.DELETE, 1 shl 6), + DELETE_EXPORT_TEXTURES(R.string.app_action_export_textures, true, AppActionGroup.DELETE, 1 shl 7), + DELETE_IMPORT_TEXTURES(R.string.app_action_import_textures, true, AppActionGroup.DELETE, 1 shl 8), + RESET_LAST_PLAYED(R.string.app_action_reset_last_played, false, AppActionGroup.OTHER, 1 shl 9); + + companion object { + val all: Set = entries.toSet() + + fun fromMask(mask: Int): Set { + return entries.filterTo(linkedSetOf()) { action -> + (mask and action.maskBit) != 0 + } + } + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppsListViewModel.kt b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppsListViewModel.kt new file mode 100644 index 000000000..b31e22710 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/AppsListViewModel.kt @@ -0,0 +1,344 @@ +package org.vita3k.emulator.ui.viewmodel + +import android.app.Application +import androidx.annotation.PluralsRes +import androidx.annotation.StringRes +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch +import org.vita3k.emulator.NativeLib +import org.vita3k.emulator.R +import org.vita3k.emulator.data.FirmwareInstallState +import org.vita3k.emulator.data.AppInfo +import org.vita3k.emulator.data.AppRepository +import org.vita3k.emulator.data.SortOption +import org.vita3k.emulator.data.UpdateCheckResult +import org.vita3k.emulator.data.UpdateCheckStatus +import org.vita3k.emulator.data.ViewMode + +class AppsListViewModel(application: Application) : AndroidViewModel(application) { + + private fun str(@StringRes id: Int, vararg args: Any): String = + getApplication().getString(id, *args) + + private fun qty(@PluralsRes id: Int, quantity: Int, vararg args: Any): String = + getApplication().resources.getQuantityString(id, quantity, *args) + + private val _allApps = mutableListOf() + val apps = mutableStateListOf() + private val _selectedAppIds = mutableStateListOf() + // Stable snapshot of selected IDs avoids .toSet() allocation on every recomposition. + val selectedAppIds: Set by derivedStateOf { _selectedAppIds.toHashSet() } + + private val availableActionsByTitleId = mutableStateMapOf>() + private var compatSyncStarted = false + private var compatSyncInProgress = false + private var startupUpdateCheckStarted = false + + var initialized by mutableStateOf(false) + private set + var loading by mutableStateOf(false) + private set + var firmwareInstallState by mutableStateOf(FirmwareInstallState.Missing) + private set + var actionInProgress by mutableStateOf(false) + private set + var actionResultMessage by mutableStateOf(null) + private set + var selectionMode by mutableStateOf(false) + private set + + var searchQuery by mutableStateOf("") + private set + var sortOption by mutableStateOf(SortOption.TITLE) + private set + var viewMode by mutableStateOf(ViewMode.LIST) + private set + var appVersion by mutableStateOf("") + private set + var updateCheckInProgress by mutableStateOf(false) + private set + var updateCheckResult by mutableStateOf(null) + private set + + // --- App info dialog --- + var infoDialogApp by mutableStateOf(null) + private set + var infoAppInstallSizeBytes by mutableStateOf(-1L) + private set + + fun initialize(storagePath: String) { + if (initialized || loading) return + + loading = true + viewModelScope.launch { + appVersion = AppRepository.getAppVersion() + val success = AppRepository.initialize(storagePath) + initialized = success + if (success) { + firmwareInstallState = AppRepository.getFirmwareInstallState() + loadApps() + startCompatibilitySync() + startUpdateCheckIfEnabled() + } + loading = false + } + } + + fun refreshAppsList(syncCompatibility: Boolean = false) { + if (!initialized) return + viewModelScope.launch { + loading = true + AppRepository.refreshAppsList() + if (syncCompatibility) { + syncCompatibilityDatabase() + } + firmwareInstallState = AppRepository.getFirmwareInstallState() + loadApps() + loading = false + } + } + + fun reloadAppsList() { + if (!initialized) return + viewModelScope.launch { + loading = true + firmwareInstallState = AppRepository.getFirmwareInstallState() + loadApps() + loading = false + } + } + + fun setSearch(query: String) { + searchQuery = query + applyFilterAndSort() + } + + fun setSort(option: SortOption) { + sortOption = option + applyFilterAndSort() + } + + fun toggleViewMode() { + viewMode = if (viewMode == ViewMode.LIST) ViewMode.GRID else ViewMode.LIST + } + + fun dismissActionResult() { + actionResultMessage = null + } + + fun dismissUpdateCheckResult() { + updateCheckResult = null + } + + // Returns the actions available for a title, defaulting to emptySet() until the + // async fetch completes (prevents showing all actions as enabled before data arrives). + fun getAvailableActions(titleId: String): Set { + return availableActionsByTitleId[titleId] ?: emptySet() + } + + fun prepareAppActions(app: AppInfo) { + if (!initialized) return + viewModelScope.launch { + availableActionsByTitleId[app.titleId] = AppRepository.getAvailableAppActions(app.titleId) + } + } + + fun updateSelectionMode(enabled: Boolean) { + selectionMode = enabled + if (!enabled) { + _selectedAppIds.clear() + } + } + + fun toggleAppSelection(app: AppInfo) { + if (!selectionMode) selectionMode = true + if (_selectedAppIds.contains(app.titleId)) { + _selectedAppIds.remove(app.titleId) + } else { + _selectedAppIds.add(app.titleId) + } + if (_selectedAppIds.isEmpty()) selectionMode = false + } + + fun selectAllVisibleApps() { + if (apps.isEmpty()) { + selectionMode = false + _selectedAppIds.clear() + return + } + selectionMode = true + _selectedAppIds.clear() + _selectedAppIds.addAll(apps.map { it.titleId }) + } + + fun runBatchDeleteSelected() { + if (!initialized || actionInProgress || _selectedAppIds.isEmpty()) return + + runActionWithProgress { + var successCount = 0 + val selected = _selectedAppIds.toList() + for (titleId in selected) { + if (AppRepository.runAppAction(titleId, AppAction.DELETE_APPLICATION)) { + successCount++ + } + } + refreshAppsList() + _selectedAppIds.clear() + selectionMode = false + if (successCount == selected.size) + qty(R.plurals.batch_delete_success, successCount, successCount) + else + str(R.string.batch_delete_partial, successCount, selected.size) + } + } + + fun runAppAction(app: AppInfo, action: AppAction) { + if (!initialized || actionInProgress) return + + runActionWithProgress { + val success = AppRepository.runAppAction(app.titleId, action) + if (success) { + if (action.group == AppActionGroup.DELETE) refreshAppsList() + availableActionsByTitleId[app.titleId] = AppRepository.getAvailableAppActions(app.titleId) + str(R.string.action_deleted_success, str(action.labelResId), app.title) + } else { + str(R.string.action_deleted_failed, str(action.labelResId), app.title) + } + } + } + + fun showAppInfo(app: AppInfo) { + infoDialogApp = app + infoAppInstallSizeBytes = -1L + viewModelScope.launch { + infoAppInstallSizeBytes = AppRepository.getAppInstallSize(app.titleId) + } + } + + fun dismissAppInfo() { + infoDialogApp = null + } + + fun checkForUpdates(manual: Boolean = true) { + if (updateCheckInProgress) { + if (manual) { + updateCheckResult = UpdateCheckResult( + status = UpdateCheckStatus.Failed, + message = str(R.string.updates_check_in_progress), + currentDisplayVersion = appVersion + ) + } + return + } + + updateCheckInProgress = true + viewModelScope.launch { + try { + val result = AppRepository.checkForUpdates( + appVersion = appVersion, + officialBuild = NativeLib.isOfficialBuild() + ) + if (manual || result.status == UpdateCheckStatus.UpdateAvailable || result.status == UpdateCheckStatus.CustomBuildCanUpdate) { + updateCheckResult = result + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + if (manual) { + updateCheckResult = UpdateCheckResult( + status = UpdateCheckStatus.Failed, + message = str(R.string.install_error_generic, e.message ?: ""), + currentDisplayVersion = appVersion + ) + } + } finally { + updateCheckInProgress = false + } + } + } + + private fun runActionWithProgress(block: suspend () -> String) { + actionInProgress = true + viewModelScope.launch { + try { + actionResultMessage = block() + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + actionResultMessage = str(R.string.install_error_generic, e.message ?: "") + } finally { + actionInProgress = false + } + } + } + + private suspend fun loadApps() { + val list = AppRepository.getAppList() + _allApps.clear() + _allApps.addAll(list) + applyFilterAndSort() + } + + private fun startCompatibilitySync() { + if (compatSyncStarted) return + + compatSyncStarted = true + viewModelScope.launch { + if (syncCompatibilityDatabase()) { + loadApps() + } + } + } + + private fun startUpdateCheckIfEnabled() { + if (startupUpdateCheckStarted) return + + startupUpdateCheckStarted = true + viewModelScope.launch { + val enabled = runCatching { NativeLib.getGlobalConfig().checkForUpdatesMode != 0 }.getOrDefault(true) + if (enabled) { + checkForUpdates(manual = false) + } + } + } + + private suspend fun syncCompatibilityDatabase(): Boolean { + if (compatSyncInProgress) return false + + compatSyncInProgress = true + return try { + AppRepository.syncCompatibilityDatabase() + } finally { + compatSyncInProgress = false + } + } + + private fun applyFilterAndSort() { + val filtered = if (searchQuery.isBlank()) { + _allApps + } else { + _allApps.filter { + it.title.contains(searchQuery, ignoreCase = true) || + it.titleId.contains(searchQuery, ignoreCase = true) + } + } + + val sorted = when (sortOption) { + SortOption.TITLE -> filtered.sortedBy { it.title.lowercase() } + SortOption.LAST_PLAYED -> filtered.sortedByDescending { it.lastPlayed } + SortOption.COMPATIBILITY -> filtered.sortedByDescending { it.compatibility.value } + } + + apps.clear() + apps.addAll(sorted) + } + +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/EmulationSessionViewModel.kt b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/EmulationSessionViewModel.kt new file mode 100644 index 000000000..9fd7c865f --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/EmulationSessionViewModel.kt @@ -0,0 +1,315 @@ +package org.vita3k.emulator.ui.viewmodel + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import org.vita3k.emulator.Emulator +import org.vita3k.emulator.NativeLib +import org.vita3k.emulator.data.NativeImeState +import org.vita3k.emulator.overlay.DEFAULT_OVERLAY_MASK +import org.vita3k.emulator.overlay.OverlayConfig +import org.vita3k.emulator.overlay.OverlayConfigStore + +data class EmulationSessionUiState( + val titleId: String = "", + val gameTitle: String = "", + val showMenu: Boolean = false, + val isPaused: Boolean = false, + val isEditingControls: Boolean = false, + val showExitConfirmation: Boolean = false, + val controlsVisible: Boolean = true, + val overlayMask: Int = DEFAULT_OVERLAY_MASK, + val l2r2Visible: Boolean = false, + val l3r3Visible: Boolean = false, + val touchSwitchVisible: Boolean = true, + val hideToggleVisible: Boolean = false, + val overlayScale: Int = 100, + val overlayOpacity: Int = 100, + val hideOverlayWhenControllerConnected: Boolean = true, + val controllerConnected: Boolean = false, + val statusMessage: String? = null +) + +class EmulationSessionViewModel(application: Application) : AndroidViewModel(application) { + + private var initialized = false + + var uiState by mutableStateOf(EmulationSessionUiState()) + private set + + var imeState by mutableStateOf(null) + private set + + fun initialize(titleId: String?, gameTitle: String?) { + if (initialized && uiState.titleId == titleId.orEmpty()) { + return + } + + val overlayConfig = OverlayConfigStore.load(getApplication()) + uiState = uiState.withOverlayConfig(overlayConfig).copy( + titleId = titleId.orEmpty(), + gameTitle = gameTitle.orEmpty() + ) + initialized = true + } + + fun updateRunningApp(titleId: String?, gameTitle: String?) { + uiState = uiState.copy( + titleId = titleId.orEmpty(), + gameTitle = gameTitle.orEmpty() + ) + initialized = true + } + + fun applyOverlayState(emulator: Emulator) { + emulator.setControllerOverlayScale(uiState.overlayScale / 100f) + emulator.setControllerOverlayOpacity(uiState.overlayOpacity) + val visibleMask = when { + uiState.showMenu -> 0 + uiState.isEditingControls -> activeOverlayMask() + uiState.hideOverlayWhenControllerConnected && uiState.controllerConnected -> 0 + else -> uiState.overlayMask + } + emulator.setControllerOverlayState(visibleMask, uiState.isEditingControls, false) + } + + fun handleBackPressed(emulator: Emulator): Boolean { + return when { + uiState.isEditingControls -> { + finishControlsEditor(emulator) + true + } + uiState.showMenu -> { + closeMenu(emulator) + true + } + else -> { + openMenu(emulator, pauseGame = false) + true + } + } + } + + @JvmOverloads + fun openMenu(emulator: Emulator, pauseGame: Boolean = true) { + if (uiState.showMenu) { + return + } + + emulator.prepareImeForPauseMenu() + emulator.ensurePauseMenuOnTop() + releaseInputs(emulator) + val paused = if (pauseGame) { + if (!setNativePauseReasonEnabled(NativeLib.PAUSE_REASON_MENU, true)) { + return + } + true + } else { + isNativeAppPaused() + } + if (!setNativeInputIntercepted(true)) { + if (pauseGame) { + setNativePauseReasonEnabled(NativeLib.PAUSE_REASON_MENU, false) + } + return + } + uiState = uiState.copy( + showMenu = true, + isPaused = paused, + isEditingControls = false, + showExitConfirmation = false, + statusMessage = null + ) + emulator.setControllerOverlayState(0, false, false) + } + + fun closeMenu(emulator: Emulator) { + releaseInputs(emulator) + if (uiState.isPaused) { + setNativePauseReasonEnabled(NativeLib.PAUSE_REASON_MENU, false) + } + setNativeInputIntercepted(false) + uiState = uiState.copy( + showMenu = false, + isPaused = false, + isEditingControls = false, + showExitConfirmation = false, + statusMessage = null + ) + applyOverlayState(emulator) + emulator.restoreImeAfterPauseMenu() + } + + fun togglePause(emulator: Emulator) { + if (uiState.isPaused) { + closeMenu(emulator) + } else { + releaseInputs(emulator) + if (!setNativePauseReasonEnabled(NativeLib.PAUSE_REASON_MENU, true)) { + return + } + uiState = uiState.copy(isPaused = true, statusMessage = null) + emulator.setControllerOverlayState(0, false, false) + } + } + + fun requestExit() { + uiState = uiState.copy(showExitConfirmation = true) + } + + fun dismissExitConfirmation() { + uiState = uiState.copy(showExitConfirmation = false) + } + + fun confirmExit(emulator: Emulator) { + releaseInputs(emulator) + uiState = uiState.copy(showExitConfirmation = false, statusMessage = null) + emulator.requestNativeQuit() + } + + fun setControlsVisible(emulator: Emulator, visible: Boolean) { + updateOverlayConfig(emulator) { it.withControlsVisible(visible) } + } + + fun setL2R2Visible(emulator: Emulator, visible: Boolean) { + updateOverlayConfig(emulator) { it.withL2R2Visible(visible) } + } + + fun setL3R3Visible(emulator: Emulator, visible: Boolean) { + updateOverlayConfig(emulator) { it.withL3R3Visible(visible) } + } + + fun setTouchSwitchVisible(emulator: Emulator, visible: Boolean) { + updateOverlayConfig(emulator) { it.withTouchSwitchVisible(visible) } + } + + fun setHideToggleVisible(emulator: Emulator, visible: Boolean) { + updateOverlayConfig(emulator) { it.withHideToggleVisible(visible) } + } + + fun setOverlayScale(emulator: Emulator, value: Int) { + updateOverlayConfig(emulator) { it.copy(overlayScale = value) } + } + + fun setOverlayOpacity(emulator: Emulator, value: Int) { + updateOverlayConfig(emulator) { it.copy(overlayOpacity = value) } + } + + fun setControllerConnected(emulator: Emulator, connected: Boolean) { + if (uiState.controllerConnected == connected) { + return + } + + uiState = uiState.copy(controllerConnected = connected) + applyOverlayState(emulator) + } + + fun startControlsEditor(emulator: Emulator) { + val mask = activeOverlayMask() + releaseInputs(emulator) + if (!setNativePauseReasonEnabled(NativeLib.PAUSE_REASON_MENU, true)) { + return + } + persistOverlayConfig(currentOverlayConfig().copy(overlayMask = mask)) + uiState = uiState.copy( + showMenu = false, + isPaused = true, + isEditingControls = true, + controlsVisible = true, + overlayMask = mask, + showExitConfirmation = false, + statusMessage = null + ) + emulator.setControllerOverlayState(mask, true, false) + } + + fun finishControlsEditor(emulator: Emulator) { + releaseInputs(emulator) + uiState = uiState.copy( + showMenu = true, + isPaused = true, + isEditingControls = false, + statusMessage = null + ) + emulator.ensurePauseMenuOnTop() + emulator.setControllerOverlayState(0, false, false) + } + + fun resetControlsLayout(emulator: Emulator) { + emulator.setControllerOverlayState(activeOverlayMask(), uiState.isEditingControls, true) + uiState = uiState.copy(statusMessage = null) + } + + fun clearStatusMessage() { + uiState = uiState.copy(statusMessage = null) + } + + fun showStatusMessage(message: String) { + uiState = uiState.copy(statusMessage = message) + } + + fun updateImeState(state: NativeImeState?) { + if (imeState == state) { + return + } + + imeState = state + } + + fun currentOverlayConfig(): OverlayConfig { + return OverlayConfig( + overlayMask = uiState.overlayMask, + overlayScale = uiState.overlayScale, + overlayOpacity = uiState.overlayOpacity, + hideOverlayWhenControllerConnected = uiState.hideOverlayWhenControllerConnected + ).normalized() + } + + fun updateOverlayConfig(emulator: Emulator, transform: (OverlayConfig) -> OverlayConfig) { + val updatedConfig = transform(currentOverlayConfig()).normalized() + persistOverlayConfig(updatedConfig) + uiState = uiState.withOverlayConfig(updatedConfig) + applyOverlayState(emulator) + } + + private fun activeOverlayMask(): Int { + return if (uiState.overlayMask == 0) DEFAULT_OVERLAY_MASK else uiState.overlayMask + } + + private fun persistOverlayConfig(config: OverlayConfig) { + OverlayConfigStore.save(getApplication(), config) + } + + private fun releaseInputs(emulator: Emulator) { + runCatching { emulator.releaseControllerOverlayInputs() } + } + + private fun setNativePauseReasonEnabled(reasonMask: Int, enabled: Boolean): Boolean { + return runCatching { NativeLib.setPauseReasonEnabled(reasonMask, enabled) }.getOrDefault(false) + } + + private fun setNativeInputIntercepted(intercepted: Boolean): Boolean { + return runCatching { NativeLib.setInputIntercepted(intercepted) }.getOrDefault(false) + } + + private fun isNativeAppPaused(): Boolean { + return runCatching { NativeLib.isAppPaused() }.getOrDefault(false) + } + + private fun EmulationSessionUiState.withOverlayConfig(config: OverlayConfig): EmulationSessionUiState { + val normalized = config.normalized() + return copy( + controlsVisible = normalized.controlsVisible, + overlayMask = normalized.overlayMask, + l2r2Visible = normalized.l2r2Visible, + l3r3Visible = normalized.l3r3Visible, + touchSwitchVisible = normalized.touchSwitchVisible, + hideToggleVisible = normalized.hideToggleVisible, + overlayScale = normalized.overlayScale, + overlayOpacity = normalized.overlayOpacity, + hideOverlayWhenControllerConnected = normalized.hideOverlayWhenControllerConnected + ) + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/InstallViewModel.kt b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/InstallViewModel.kt new file mode 100644 index 000000000..5012a0d34 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/InstallViewModel.kt @@ -0,0 +1,247 @@ +package org.vita3k.emulator.ui.viewmodel + +import android.app.Application +import androidx.annotation.StringRes +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.vita3k.emulator.InstallServiceController +import org.vita3k.emulator.R +import org.vita3k.emulator.data.InstallRepository +import java.io.File + +enum class InstallType { + FIRMWARE, PKG, ARCHIVE, LICENSE +} + +enum class InstallResultStatus { + SUCCESS, PARTIAL, ERROR +} + +data class DeleteSourceOption( + val label: String, + val paths: List +) + +data class InstallResult( + val status: InstallResultStatus, + val message: String, + val deleteOptions: List = emptyList() +) + +class InstallViewModel(application: Application) : AndroidViewModel(application) { + + private fun str(@StringRes id: Int, vararg args: Any): String = + getApplication().getString(id, *args) + + var showInstallSheet by mutableStateOf(false) + private set + var installing by mutableStateOf(false) + private set + var progress by mutableStateOf(0) + private set + var statusMessage by mutableStateOf("") + private set + var installResult by mutableStateOf(null) + private set + var showPkgLicenseDialog by mutableStateOf(false) + private set + var completedInstallToken by mutableStateOf(0L) + private set + + private var pendingPkgPath: String? = null + private var pendingPkgLicensePath: String? = null + private var lastObservedCompletionId = 0L + + init { + viewModelScope.launch { + InstallServiceController.state.collectLatest { state -> + if (state.operationId == 0L) { + return@collectLatest + } + + installing = state.installing + progress = state.progress + statusMessage = state.statusMessage + installResult = state.installResult + + if (!state.installing && state.installResult != null && state.operationId != lastObservedCompletionId) { + lastObservedCompletionId = state.operationId + completedInstallToken = state.operationId + } + } + } + } + + fun showSheet() { + showInstallSheet = true + } + + fun hideSheet() { + showInstallSheet = false + } + + fun confirmInstallResult(selectedDeleteOptions: List) { + val paths = selectedDeleteOptions + .flatMap(DeleteSourceOption::paths) + .filter(String::isNotBlank) + .distinct() + + installResult = null + InstallServiceController.clearResult() + if (paths.isEmpty()) { + return + } + + viewModelScope.launch { + paths.forEach { path -> + runCatching { File(path).delete() } + } + } + } + + fun installFirmware(path: String) { + InstallServiceController.clearResult() + InstallServiceController.startFirmware(getApplication(), path) + } + + fun onPkgPicked(path: String) { + InstallServiceController.clearResult() + pendingPkgLicensePath = null + beginInstall(R.string.install_status_preparing) + viewModelScope.launch { + try { + pendingPkgPath = path + val autoZrif = InstallRepository.findPkgZrif(path) + installing = false + if (autoZrif.isNotEmpty()) { + confirmPkgInstall(autoZrif) + } else { + showPkgLicenseDialog = true + } + } catch (e: Exception) { + pendingPkgPath = null + pendingPkgLicensePath = null + installing = false + installResult = InstallResult( + status = InstallResultStatus.ERROR, + message = str(R.string.install_error_generic, e.message ?: ""), + deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_package_file, path) + ) + ) + } + } + } + + fun onPkgLicenseFilePicked(path: String) { + pendingPkgLicensePath = path + viewModelScope.launch { + try { + val zrif = InstallRepository.convertRifToZrif(path) + if (zrif.isEmpty()) { + installResult = InstallResult( + status = InstallResultStatus.ERROR, + message = str(R.string.install_error_read_license), + deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_package_file, pendingPkgPath), + deleteOption(R.string.install_delete_license_file, path) + ) + ) + cancelPkgInstall() + return@launch + } + + confirmPkgInstall(zrif) + } catch (e: Exception) { + installResult = InstallResult( + status = InstallResultStatus.ERROR, + message = str(R.string.install_error_generic, e.message ?: ""), + deleteOptions = sourceDeleteOptions( + deleteOption(R.string.install_delete_package_file, pendingPkgPath), + deleteOption(R.string.install_delete_license_file, path) + ) + ) + cancelPkgInstall() + } + } + } + + fun confirmPkgInstall(zrif: String) { + showPkgLicenseDialog = false + val path = pendingPkgPath ?: run { + installResult = InstallResult( + status = InstallResultStatus.ERROR, + message = str(R.string.install_error_pkg_path_lost) + ) + pendingPkgLicensePath = null + return + } + val licensePath = pendingPkgLicensePath + pendingPkgPath = null + pendingPkgLicensePath = null + InstallServiceController.clearResult() + InstallServiceController.startPkg(getApplication(), path, zrif, licensePath) + } + + fun cancelPkgInstall() { + showPkgLicenseDialog = false + pendingPkgPath = null + pendingPkgLicensePath = null + } + + fun installArchive(path: String) { + InstallServiceController.clearResult() + InstallServiceController.startArchive(getApplication(), path) + } + + fun installArchiveFolder(paths: List) { + InstallServiceController.clearResult() + InstallServiceController.startArchiveFolder(getApplication(), paths) + } + + fun installLicense(path: String) { + InstallServiceController.clearResult() + InstallServiceController.startLicenseFile(getApplication(), path) + } + + fun installLicenseFromZrif(zrif: String) { + InstallServiceController.clearResult() + InstallServiceController.startLicenseZrif(getApplication(), zrif) + } + + private fun beginInstall(@StringRes statusResId: Int) { + installResult = null + installing = true + progress = 0 + statusMessage = str(statusResId) + } + + private fun deleteOption(@StringRes labelResId: Int, path: String?): DeleteSourceOption? = + deleteOption(labelResId, listOfNotNull(path)) + + private fun deleteOption( + @StringRes labelResId: Int, + paths: List + ): DeleteSourceOption? { + val normalized = paths + .filter(String::isNotBlank) + .distinct() + if (normalized.isEmpty()) { + return null + } + + return DeleteSourceOption( + label = str(labelResId), + paths = normalized + ) + } + + private fun sourceDeleteOptions(vararg options: DeleteSourceOption?): List = + options.filterNotNull() + +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/SettingsViewModel.kt b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/SettingsViewModel.kt new file mode 100644 index 000000000..c77156595 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/SettingsViewModel.kt @@ -0,0 +1,617 @@ +package org.vita3k.emulator.ui.viewmodel + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.vita3k.emulator.NativeLib +import org.vita3k.emulator.R +import org.vita3k.emulator.data.AppStorage +import org.vita3k.emulator.data.CustomDriverLoadStatus +import org.vita3k.emulator.data.CustomDriverSupportInfo +import org.vita3k.emulator.data.EmulatorConfig +import org.vita3k.emulator.data.RestartRequiredSetting +import org.vita3k.emulator.data.SettingsRepository +import org.vita3k.emulator.data.SettingsSnapshot +import org.vita3k.emulator.data.UiLanguages + +data class SettingsOperationResult( + val message: String, + val isError: Boolean +) + +private const val GLOBAL_SETTINGS_ROUTE_KEY = "__global__" + +class SettingsViewModel(application: Application) : AndroidViewModel(application) { + + private fun initialConfig(): EmulatorConfig = + if (NativeLib.isInitialized()) NativeLib.getDefaultConfig() else EmulatorConfig() + + private fun str(resId: Int, vararg args: Any): String = + getApplication().getString(resId, *args) + + private var loadJob: Job? = null + private var memoryMappingRefreshJob: Job? = null + private var loadRequestId = 0 + private var memoryMappingRefreshRequestId = 0 + private var activeLoadRouteKey: String? = null + private var loadedRouteKey by mutableStateOf(null) + + var config by mutableStateOf(initialConfig()) + private set + + var currentStoragePath by mutableStateOf( + if (NativeLib.isInitialized()) NativeLib.getCurrentEmulatorPath() else AppStorage.defaultStoragePath(getApplication()) + ) + private set + + val defaultStoragePath: String = AppStorage.defaultStoragePath(getApplication()) + + private var originalConfig by mutableStateOf(config.copy()) + private var originalModulesList: List> = emptyList() + + var titleId: String? = null + private set + + var loading by mutableStateOf(false) + private set + + var saving by mutableStateOf(false) + private set + + var hasCustomConfig by mutableStateOf(false) + private set + + var installedCustomDrivers by mutableStateOf>(emptyList()) + private set + + var showCustomDriverOptions by mutableStateOf(true) + private set + + var customDriverBusy by mutableStateOf(false) + private set + + var operationResult by mutableStateOf(null) + private set + + var supportedMemoryMappingMask by mutableStateOf(1) + private set + + var customDriverLoadStatus by mutableStateOf(CustomDriverLoadStatus.Default) + private set + + var availableCameras by mutableStateOf>(emptyList()) + private set + + var availableAdhocAddresses by mutableStateOf>>(emptyList()) + private set + + val isDirty: Boolean + get() = config != originalConfig + + var modulesList by mutableStateOf>>(emptyList()) + private set + + var modulesSearch by mutableStateOf("") + private set + + fun isLoaded(titleId: String?): Boolean = loadedRouteKey == routeKey(titleId) + + fun preloadGlobalSettings() { + load(titleId = null) + } + + fun load(titleId: String?, force: Boolean = false) { + val routeKey = routeKey(titleId) + this.titleId = titleId + if (!force && loading && activeLoadRouteKey == routeKey) { + return + } + + loadJob?.cancel() + val requestId = ++loadRequestId + activeLoadRouteKey = routeKey + loading = true + loadJob = viewModelScope.launch { + try { + val snapshot = withContext(Dispatchers.IO) { + SettingsRepository.load(titleId) + } + if (loadRequestId != requestId) return@launch + applySnapshot(titleId, routeKey, snapshot) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + if (loadRequestId == requestId) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } + } finally { + if (loadRequestId == requestId) { + loading = false + activeLoadRouteKey = null + loadJob = null + } + } + } + } + + fun save(forceCustomConfig: Boolean = false, onSaved: ((List) -> Unit)? = null) { + if (saving) return + val configSnapshot = config.copy() + val routeTitleId = titleId + val saveTitleId = routeTitleId?.takeIf { forceCustomConfig || hasCustomConfig } + saving = true + viewModelScope.launch { + var saveSucceeded = false + var restartRequiredSettings: List = emptyList() + try { + val saveResult = withContext(Dispatchers.IO) { + SettingsRepository.save(routeTitleId, saveTitleId, configSnapshot) + } + hasCustomConfig = saveResult.hasCustomConfig + restartRequiredSettings = saveResult.restartRequiredSettings + originalConfig = configSnapshot.copy() + originalModulesList = modulesList + loadedRouteKey = routeKey(routeTitleId) + saveSucceeded = true + if (saveTitleId == null) { + UiLanguages.applyAndPersist(getApplication(), configSnapshot.userLang) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } finally { + saving = false + } + if (saveSucceeded) { + onSaved?.invoke(restartRequiredSettings) + } + } + } + + fun changeStorageFolder(storagePath: String, onStorageChanged: () -> Unit) { + viewModelScope.launch { + try { + if (storagePath.isNullOrBlank()) { + operationResult = SettingsOperationResult( + str(R.string.settings_emulator_storage_folder_resolve_failed), + true + ) + return@launch + } + + val success = withContext(Dispatchers.IO) { + SettingsRepository.setCurrentEmulatorPath(storagePath) + } + if (!success) { + operationResult = SettingsOperationResult( + str(R.string.settings_emulator_storage_folder_change_failed), + true + ) + return@launch + } + val routeKey = routeKey(titleId) + val snapshot = withContext(Dispatchers.IO) { + SettingsRepository.load(titleId) + } + applySnapshot(titleId, routeKey, snapshot) + onStorageChanged() + operationResult = SettingsOperationResult( + str(R.string.settings_emulator_storage_folder_change_success, storagePath), + false + ) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } + } + } + + fun resetStorageFolder(onStorageChanged: () -> Unit) { + changeStorageFolder(defaultStoragePath, onStorageChanged) + } + + fun resetToDefaults() { + viewModelScope.launch { + try { + val defaults = withContext(Dispatchers.IO) { + SettingsRepository.loadDefaults() + } + config = defaults.config.copy() + modulesList = defaults.modulesList + installedCustomDrivers = defaults.installedCustomDrivers + supportedMemoryMappingMask = defaults.supportedMemoryMappingMask + customDriverLoadStatus = defaults.customDriverLoadStatus + availableCameras = defaults.availableCameras + availableAdhocAddresses = defaults.availableAdhocAddresses + operationResult = SettingsOperationResult( + str(R.string.settings_reset_defaults_done), + false + ) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } + } + } + + fun deleteCustomConfig(onDeleted: ((List) -> Unit)? = null) { + val id = titleId ?: return + viewModelScope.launch { + val result = withContext(Dispatchers.IO) { + SettingsRepository.deleteCustomConfig(id) + } + applySnapshot(id, routeKey(id), result.snapshot) + onDeleted?.invoke(result.restartRequiredSettings) + } + } + + fun discardChanges() { + config = originalConfig.copy() + modulesList = originalModulesList + modulesSearch = "" + } + + fun clearAllCustomConfigs() { + viewModelScope.launch { + val deletedCount = withContext(Dispatchers.IO) { + SettingsRepository.clearAllCustomConfigs() + } + operationResult = SettingsOperationResult( + str(R.string.settings_clear_all_custom_configs_success, deletedCount), + false + ) + } + } + + fun installCustomDriver(path: String) { + if (customDriverBusy) return + + customDriverBusy = true + viewModelScope.launch { + try { + if (path.isBlank()) { + operationResult = SettingsOperationResult( + str(R.string.settings_gpu_custom_driver_resolve_failed), + true + ) + return@launch + } + + val installedName = withContext(Dispatchers.IO) { + SettingsRepository.installCustomDriver(path) + } + if (installedName.isEmpty()) { + operationResult = SettingsOperationResult( + str(R.string.settings_gpu_custom_driver_install_failed), + true + ) + return@launch + } + + installedCustomDrivers = withContext(Dispatchers.IO) { + SettingsRepository.getInstalledCustomDrivers() + } + val supportInfo = withContext(Dispatchers.IO) { + SettingsRepository.getCustomDriverSupportInfo(installedName) + } + applyCustomDriverSupportInfo( + requestedDriverName = installedName, + supportInfo = supportInfo, + coerceSelectionOnFallback = false + ) + operationResult = SettingsOperationResult( + if (supportInfo.loadStatus == CustomDriverLoadStatus.Loaded) { + str(R.string.settings_gpu_custom_driver_install_success, installedName) + } else { + str(R.string.settings_gpu_custom_driver_install_fallback, installedName) + }, + supportInfo.loadStatus != CustomDriverLoadStatus.Loaded + ) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } finally { + customDriverBusy = false + } + } + } + + fun removeCustomDriver(driverName: String) { + if (customDriverBusy || driverName.isEmpty()) return + + customDriverBusy = true + viewModelScope.launch { + try { + val removed = withContext(Dispatchers.IO) { + SettingsRepository.removeCustomDriver(driverName) + } + if (!removed) { + operationResult = SettingsOperationResult( + str(R.string.settings_gpu_custom_driver_remove_failed, driverName), + true + ) + return@launch + } + + installedCustomDrivers = withContext(Dispatchers.IO) { + SettingsRepository.getInstalledCustomDrivers() + } + if (config.customDriverName == driverName) { + val supportInfo = withContext(Dispatchers.IO) { + SettingsRepository.getCustomDriverSupportInfo("") + } + applyCustomDriverSupportInfo( + requestedDriverName = "", + supportInfo = supportInfo, + coerceSelectionOnFallback = false + ) + } + operationResult = SettingsOperationResult( + str(R.string.settings_gpu_custom_driver_remove_success, driverName), + false + ) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } finally { + customDriverBusy = false + } + } + } + + fun setCameraImage(isFront: Boolean, path: String) { + viewModelScope.launch { + try { + if (path.isNullOrBlank()) { + operationResult = SettingsOperationResult( + str(R.string.settings_camera_image_resolve_failed), + true + ) + return@launch + } + + update { + if (isFront) { + frontCameraType = 1 + frontCameraImage = path + } else { + backCameraType = 1 + backCameraImage = path + } + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } + } + } + + fun dismissOperationResult() { + operationResult = null + } + + fun selectCustomDriver(driverName: String) { + if (customDriverBusy) return + + val normalizedDriverName = driverName.trim() + if (config.customDriverName == normalizedDriverName) { + refreshSupportedMemoryMappingMask(normalizedDriverName) + return + } + + customDriverBusy = true + config = config.copy().apply { + customDriverName = normalizedDriverName + } + viewModelScope.launch { + try { + val supportInfo = withContext(Dispatchers.IO) { + SettingsRepository.getCustomDriverSupportInfo(normalizedDriverName) + } + applyCustomDriverSupportInfo( + requestedDriverName = normalizedDriverName, + supportInfo = supportInfo, + coerceSelectionOnFallback = false + ) + operationResult = when { + normalizedDriverName.isEmpty() -> null + supportInfo.loadStatus == CustomDriverLoadStatus.Loaded -> SettingsOperationResult( + str(R.string.settings_gpu_custom_driver_selected_success, normalizedDriverName), + false + ) + else -> SettingsOperationResult( + str(R.string.settings_gpu_custom_driver_selected_fallback, normalizedDriverName), + true + ) + } + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = SettingsOperationResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + val fallbackSupportInfo = CustomDriverSupportInfo( + supportedMemoryMappingMask = 1, + loadStatus = if (normalizedDriverName.isEmpty()) { + CustomDriverLoadStatus.Default + } else { + CustomDriverLoadStatus.Fallback + } + ) + applyCustomDriverSupportInfo( + requestedDriverName = normalizedDriverName, + supportInfo = fallbackSupportInfo, + coerceSelectionOnFallback = false + ) + } finally { + customDriverBusy = false + } + } + } + + fun update(block: EmulatorConfig.() -> Unit) { + val previousDriverName = config.customDriverName + val updatedConfig = config.copy().apply(block) + config = updatedConfig + + if (previousDriverName != updatedConfig.customDriverName) { + refreshSupportedMemoryMappingMask(updatedConfig.customDriverName) + } + } + + fun onModulesSearchChange(query: String) { + modulesSearch = query + } + + fun toggleModule(name: String) { + val newList = modulesList.map { (n, e) -> if (n == name) n to !e else n to e } + modulesList = newList + update { lleModules = newList.filter { it.second }.map { it.first }.toTypedArray() } + } + + private fun refreshSupportedMemoryMappingMask( + customDriverName: String, + coerceSelectionOnFallback: Boolean = false, + onApplied: ((CustomDriverSupportInfo) -> Unit)? = null + ) { + memoryMappingRefreshJob?.cancel() + val requestId = ++memoryMappingRefreshRequestId + memoryMappingRefreshJob = viewModelScope.launch { + try { + val supportInfo = withContext(Dispatchers.IO) { + SettingsRepository.getCustomDriverSupportInfo(customDriverName) + } + if (memoryMappingRefreshRequestId != requestId) return@launch + if (!coerceSelectionOnFallback && config.customDriverName != customDriverName) return@launch + + applyCustomDriverSupportInfo(customDriverName, supportInfo, coerceSelectionOnFallback) + onApplied?.invoke(supportInfo) + } catch (cancelled: CancellationException) { + throw cancelled + } catch (_: Exception) { + if (memoryMappingRefreshRequestId != requestId) return@launch + if (!coerceSelectionOnFallback && config.customDriverName != customDriverName) return@launch + + val fallbackSupportInfo = CustomDriverSupportInfo( + supportedMemoryMappingMask = 1, + loadStatus = if (customDriverName.isEmpty()) { + CustomDriverLoadStatus.Default + } else { + CustomDriverLoadStatus.Fallback + } + ) + applyCustomDriverSupportInfo(customDriverName, fallbackSupportInfo, coerceSelectionOnFallback) + onApplied?.invoke(fallbackSupportInfo) + } + } + } + + private fun applyCustomDriverSupportInfo( + requestedDriverName: String, + supportInfo: CustomDriverSupportInfo, + coerceSelectionOnFallback: Boolean + ) { + val shouldFallbackToDefault = requestedDriverName.isNotEmpty() && + supportInfo.loadStatus == CustomDriverLoadStatus.Fallback && + coerceSelectionOnFallback + val effectiveDriverName = if (shouldFallbackToDefault) "" else requestedDriverName + val effectiveLoadStatus = when { + effectiveDriverName.isEmpty() -> CustomDriverLoadStatus.Default + else -> supportInfo.loadStatus + } + + supportedMemoryMappingMask = supportInfo.supportedMemoryMappingMask + customDriverLoadStatus = effectiveLoadStatus + + val coercedMapping = coerceMemoryMapping(config.memoryMapping, supportInfo.supportedMemoryMappingMask) + val currentConfig = config + if (currentConfig.customDriverName != effectiveDriverName || currentConfig.memoryMapping != coercedMapping) { + config = currentConfig.copy().apply { + customDriverName = effectiveDriverName + memoryMapping = coercedMapping + } + } + } + + private fun coerceMemoryMapping(memoryMapping: String, supportedMask: Int): String { + if (isMemoryMappingSupported(memoryMapping, supportedMask)) { + return memoryMapping + } + + return when { + (supportedMask and (1 shl 1)) != 0 -> "double-buffer" + (supportedMask and (1 shl 2)) != 0 -> "external-host" + (supportedMask and (1 shl 3)) != 0 -> "page-table" + (supportedMask and (1 shl 4)) != 0 -> "native-buffer" + else -> "disabled" + } + } + + private fun isMemoryMappingSupported(memoryMapping: String, supportedMask: Int): Boolean { + val bit = when (memoryMapping) { + "disabled" -> 0 + "double-buffer" -> 1 + "external-host" -> 2 + "page-table" -> 3 + "native-buffer" -> 4 + else -> return false + } + + return (supportedMask and (1 shl bit)) != 0 + } + + private fun routeKey(titleId: String?): String = titleId ?: GLOBAL_SETTINGS_ROUTE_KEY + + private fun applySnapshot(titleId: String?, routeKey: String, snapshot: SettingsSnapshot) { + this.titleId = titleId + currentStoragePath = snapshot.emulatorStoragePath + hasCustomConfig = snapshot.hasCustomConfig + originalConfig = snapshot.config.copy() + originalModulesList = snapshot.modulesList + config = snapshot.config.copy() + modulesList = snapshot.modulesList + installedCustomDrivers = snapshot.installedCustomDrivers + showCustomDriverOptions = snapshot.showCustomDriverOptions + supportedMemoryMappingMask = snapshot.supportedMemoryMappingMask + customDriverLoadStatus = snapshot.customDriverLoadStatus + availableCameras = snapshot.availableCameras + availableAdhocAddresses = snapshot.availableAdhocAddresses + loadedRouteKey = routeKey + } +} diff --git a/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/UserManagementViewModel.kt b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/UserManagementViewModel.kt new file mode 100644 index 000000000..d7f7b4406 --- /dev/null +++ b/android/app/src/main/java/org/vita3k/emulator/ui/viewmodel/UserManagementViewModel.kt @@ -0,0 +1,163 @@ +package org.vita3k.emulator.ui.viewmodel + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch +import org.vita3k.emulator.R +import org.vita3k.emulator.data.NativeUser +import org.vita3k.emulator.data.UserRepository + +data class UserManagementResult( + val message: String, + val isError: Boolean +) + +class UserManagementViewModel(application: Application) : AndroidViewModel(application) { + + private fun str(resId: Int, vararg args: Any): String = + getApplication().getString(resId, *args) + + var users by mutableStateOf>(emptyList()) + private set + + var loading by mutableStateOf(false) + private set + + var busy by mutableStateOf(false) + private set + + var operationResult by mutableStateOf(null) + private set + + fun load() { + if (loading) return + + loading = true + viewModelScope.launch { + try { + users = UserRepository.getUsers() + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = UserManagementResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } finally { + loading = false + } + } + } + + fun createUser(name: String, onUsersChanged: () -> Unit = {}) { + val trimmedName = name.trim() + if (busy || trimmedName.isEmpty()) return + + busy = true + viewModelScope.launch { + try { + val userId = UserRepository.createUser(trimmedName) + if (userId.isEmpty()) { + operationResult = UserManagementResult( + str(R.string.user_management_create_failed), + true + ) + return@launch + } + + users = UserRepository.getUsers() + operationResult = UserManagementResult( + str(R.string.user_management_created_success, trimmedName), + false + ) + onUsersChanged() + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = UserManagementResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } finally { + busy = false + } + } + } + + fun activateUser(user: NativeUser, onUsersChanged: () -> Unit = {}) { + if (busy || user.active) return + + busy = true + viewModelScope.launch { + try { + val success = UserRepository.activateUser(user.id) + if (!success) { + operationResult = UserManagementResult( + str(R.string.user_management_select_failed), + true + ) + return@launch + } + + users = UserRepository.getUsers() + operationResult = UserManagementResult( + str(R.string.user_management_selected_success, user.name), + false + ) + onUsersChanged() + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = UserManagementResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } finally { + busy = false + } + } + } + + fun deleteUser(user: NativeUser, onUsersChanged: () -> Unit = {}) { + if (busy) return + + busy = true + viewModelScope.launch { + try { + val success = UserRepository.deleteUser(user.id) + if (!success) { + operationResult = UserManagementResult( + str(R.string.user_management_delete_failed), + true + ) + return@launch + } + + users = UserRepository.getUsers() + operationResult = UserManagementResult( + str(R.string.user_management_deleted_success, user.name), + false + ) + onUsersChanged() + } catch (cancelled: CancellationException) { + throw cancelled + } catch (e: Exception) { + operationResult = UserManagementResult( + str(R.string.install_error_generic, e.message ?: ""), + true + ) + } finally { + busy = false + } + } + } + + fun dismissOperationResult() { + operationResult = null + } +} diff --git a/android/src/main/res/drawable/button_circle.png b/android/app/src/main/res/drawable/button_circle.png similarity index 100% rename from android/src/main/res/drawable/button_circle.png rename to android/app/src/main/res/drawable/button_circle.png diff --git a/android/src/main/res/drawable/button_circle_pressed.png b/android/app/src/main/res/drawable/button_circle_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_circle_pressed.png rename to android/app/src/main/res/drawable/button_circle_pressed.png diff --git a/android/src/main/res/drawable/button_cross.png b/android/app/src/main/res/drawable/button_cross.png similarity index 100% rename from android/src/main/res/drawable/button_cross.png rename to android/app/src/main/res/drawable/button_cross.png diff --git a/android/src/main/res/drawable/button_cross_pressed.png b/android/app/src/main/res/drawable/button_cross_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_cross_pressed.png rename to android/app/src/main/res/drawable/button_cross_pressed.png diff --git a/android/src/main/res/drawable/button_hide.png b/android/app/src/main/res/drawable/button_hide.png similarity index 100% rename from android/src/main/res/drawable/button_hide.png rename to android/app/src/main/res/drawable/button_hide.png diff --git a/android/src/main/res/drawable/button_hide_pressed.png b/android/app/src/main/res/drawable/button_hide_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_hide_pressed.png rename to android/app/src/main/res/drawable/button_hide_pressed.png diff --git a/android/src/main/res/drawable/button_l.png b/android/app/src/main/res/drawable/button_l.png similarity index 100% rename from android/src/main/res/drawable/button_l.png rename to android/app/src/main/res/drawable/button_l.png diff --git a/android/src/main/res/drawable/button_l2.png b/android/app/src/main/res/drawable/button_l2.png similarity index 100% rename from android/src/main/res/drawable/button_l2.png rename to android/app/src/main/res/drawable/button_l2.png diff --git a/android/src/main/res/drawable/button_l2_pressed.png b/android/app/src/main/res/drawable/button_l2_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_l2_pressed.png rename to android/app/src/main/res/drawable/button_l2_pressed.png diff --git a/android/src/main/res/drawable/button_l3.png b/android/app/src/main/res/drawable/button_l3.png similarity index 100% rename from android/src/main/res/drawable/button_l3.png rename to android/app/src/main/res/drawable/button_l3.png diff --git a/android/src/main/res/drawable/button_l3_pressed.png b/android/app/src/main/res/drawable/button_l3_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_l3_pressed.png rename to android/app/src/main/res/drawable/button_l3_pressed.png diff --git a/android/src/main/res/drawable/button_l_pressed.png b/android/app/src/main/res/drawable/button_l_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_l_pressed.png rename to android/app/src/main/res/drawable/button_l_pressed.png diff --git a/android/src/main/res/drawable/button_ps.png b/android/app/src/main/res/drawable/button_ps.png similarity index 100% rename from android/src/main/res/drawable/button_ps.png rename to android/app/src/main/res/drawable/button_ps.png diff --git a/android/src/main/res/drawable/button_ps_pressed.png b/android/app/src/main/res/drawable/button_ps_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_ps_pressed.png rename to android/app/src/main/res/drawable/button_ps_pressed.png diff --git a/android/src/main/res/drawable/button_r.png b/android/app/src/main/res/drawable/button_r.png similarity index 100% rename from android/src/main/res/drawable/button_r.png rename to android/app/src/main/res/drawable/button_r.png diff --git a/android/src/main/res/drawable/button_r2.png b/android/app/src/main/res/drawable/button_r2.png similarity index 100% rename from android/src/main/res/drawable/button_r2.png rename to android/app/src/main/res/drawable/button_r2.png diff --git a/android/src/main/res/drawable/button_r2_pressed.png b/android/app/src/main/res/drawable/button_r2_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_r2_pressed.png rename to android/app/src/main/res/drawable/button_r2_pressed.png diff --git a/android/src/main/res/drawable/button_r3.png b/android/app/src/main/res/drawable/button_r3.png similarity index 100% rename from android/src/main/res/drawable/button_r3.png rename to android/app/src/main/res/drawable/button_r3.png diff --git a/android/src/main/res/drawable/button_r3_pressed.png b/android/app/src/main/res/drawable/button_r3_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_r3_pressed.png rename to android/app/src/main/res/drawable/button_r3_pressed.png diff --git a/android/src/main/res/drawable/button_r_pressed.png b/android/app/src/main/res/drawable/button_r_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_r_pressed.png rename to android/app/src/main/res/drawable/button_r_pressed.png diff --git a/android/src/main/res/drawable/button_select.png b/android/app/src/main/res/drawable/button_select.png similarity index 100% rename from android/src/main/res/drawable/button_select.png rename to android/app/src/main/res/drawable/button_select.png diff --git a/android/src/main/res/drawable/button_select_pressed.png b/android/app/src/main/res/drawable/button_select_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_select_pressed.png rename to android/app/src/main/res/drawable/button_select_pressed.png diff --git a/android/src/main/res/drawable/button_square.png b/android/app/src/main/res/drawable/button_square.png similarity index 100% rename from android/src/main/res/drawable/button_square.png rename to android/app/src/main/res/drawable/button_square.png diff --git a/android/src/main/res/drawable/button_square_pressed.png b/android/app/src/main/res/drawable/button_square_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_square_pressed.png rename to android/app/src/main/res/drawable/button_square_pressed.png diff --git a/android/src/main/res/drawable/button_start.png b/android/app/src/main/res/drawable/button_start.png similarity index 100% rename from android/src/main/res/drawable/button_start.png rename to android/app/src/main/res/drawable/button_start.png diff --git a/android/src/main/res/drawable/button_start_pressed.png b/android/app/src/main/res/drawable/button_start_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_start_pressed.png rename to android/app/src/main/res/drawable/button_start_pressed.png diff --git a/android/src/main/res/drawable/button_touch_b.png b/android/app/src/main/res/drawable/button_touch_b.png similarity index 100% rename from android/src/main/res/drawable/button_touch_b.png rename to android/app/src/main/res/drawable/button_touch_b.png diff --git a/android/src/main/res/drawable/button_touch_f.png b/android/app/src/main/res/drawable/button_touch_f.png similarity index 100% rename from android/src/main/res/drawable/button_touch_f.png rename to android/app/src/main/res/drawable/button_touch_f.png diff --git a/android/src/main/res/drawable/button_triangle.png b/android/app/src/main/res/drawable/button_triangle.png similarity index 100% rename from android/src/main/res/drawable/button_triangle.png rename to android/app/src/main/res/drawable/button_triangle.png diff --git a/android/src/main/res/drawable/button_triangle_pressed.png b/android/app/src/main/res/drawable/button_triangle_pressed.png similarity index 100% rename from android/src/main/res/drawable/button_triangle_pressed.png rename to android/app/src/main/res/drawable/button_triangle_pressed.png diff --git a/android/src/main/res/drawable/dpad_idle.png b/android/app/src/main/res/drawable/dpad_idle.png similarity index 100% rename from android/src/main/res/drawable/dpad_idle.png rename to android/app/src/main/res/drawable/dpad_idle.png diff --git a/android/src/main/res/drawable/dpad_up.png b/android/app/src/main/res/drawable/dpad_up.png similarity index 100% rename from android/src/main/res/drawable/dpad_up.png rename to android/app/src/main/res/drawable/dpad_up.png diff --git a/android/src/main/res/drawable/dpad_up_left.png b/android/app/src/main/res/drawable/dpad_up_left.png similarity index 100% rename from android/src/main/res/drawable/dpad_up_left.png rename to android/app/src/main/res/drawable/dpad_up_left.png diff --git a/android/src/main/res/drawable/joystick.png b/android/app/src/main/res/drawable/joystick.png similarity index 100% rename from android/src/main/res/drawable/joystick.png rename to android/app/src/main/res/drawable/joystick.png diff --git a/android/src/main/res/drawable/joystick_pressed.png b/android/app/src/main/res/drawable/joystick_pressed.png similarity index 100% rename from android/src/main/res/drawable/joystick_pressed.png rename to android/app/src/main/res/drawable/joystick_pressed.png diff --git a/android/src/main/res/drawable/joystick_range.png b/android/app/src/main/res/drawable/joystick_range.png similarity index 100% rename from android/src/main/res/drawable/joystick_range.png rename to android/app/src/main/res/drawable/joystick_range.png diff --git a/android/app/src/main/res/drawable/splash_background.xml b/android/app/src/main/res/drawable/splash_background.xml new file mode 100644 index 000000000..28054ee68 --- /dev/null +++ b/android/app/src/main/res/drawable/splash_background.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/app/src/main/res/drawable/splash_icon.xml b/android/app/src/main/res/drawable/splash_icon.xml new file mode 100644 index 000000000..d7afb1506 --- /dev/null +++ b/android/app/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/android/src/main/res/mipmap/ic_launcher.png b/android/app/src/main/res/mipmap/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap/ic_launcher.png rename to android/app/src/main/res/mipmap/ic_launcher.png diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 000000000..f4b958081 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..358f9c0e0 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #111111 + diff --git a/android/src/main/res/values/integers.xml b/android/app/src/main/res/values/integers.xml similarity index 80% rename from android/src/main/res/values/integers.xml rename to android/app/src/main/res/values/integers.xml index dc271c284..7807d92b0 100644 --- a/android/src/main/res/values/integers.xml +++ b/android/app/src/main/res/values/integers.xml @@ -28,12 +28,12 @@ -45 670 -45 - 380 - -45 - 570 - -35 - 400 + 320 + 5 + 602 + 5 + 475 5 - 500 - 5 + 600 + 875 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..e3f1adf4c --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,715 @@ + + + Vita3K + + + OK + Cancel + Delete + Yes + No + Back + Install + + + Unknown + Nothing + Bootable + Intro + Menu + Ingame− + Ingame+ + Playable + + + Delete + Other + + + Application + Save Data + Patch + DLC + License + Shader Cache + Shader Log + Export Textures + Import Textures + Reset Last Played + + + Vita3K + + %1$d selected + %1$d selected + + Install + Exit selection + Select all + Delete selected + Search + Sort + Toggle view + Refresh + More options + Close search + Selected + User Management + Welcome Screen + Check for Updates + About + Search apps\u2026 + Title + Last Played + Compatibility + CC + No apps match \"%1$s\" + Missing firmware components: %1$s + Firmware Required + Install the missing PS Vita firmware components so that apps can function correctly. + Missing components + No firmware installed + Main firmware is not installed + No apps installed + Install Firmware + Install App + Failed to initialise emulator. + + + Information + Delete\u2026 + Custom Config + + + User Management + Refresh users + Current User + No user selected + Active + Create User + Create a local Vita user profile for saves, play history, and app data. + User name + Select User + Delete User + Delete User + Are you sure you want to delete user \"%1$s\"?\n\nAll user data will be lost. + No users found + Create a user to start storing saves and play history. + Created user %1$s. + Selected user %1$s. + Deleted user %1$s. + Failed to create user. + Failed to select user. + Failed to delete user. + + + + Delete %1$d Application + Delete %1$d Applications + + + Are you sure you want to delete %1$d selected application?\n\nThis action cannot be undone. + Are you sure you want to delete %1$d selected applications?\n\nThis action cannot be undone. + + + + Delete %1$s + This will permanently delete %1$s for this application. This action cannot be undone. + Reset Play Time + Are you sure you want to reset the play time for %1$s? + this app + + + Deleting %1$s\u2026 + Applying action\u2026 + Done + Reset + + Deleted %1$d application. + Deleted %1$d applications. + + Deleted %1$d of %2$d applications. + %1$s deleted for %2$s. + Failed to delete %1$s for %2$s. + + + App Information + Close + Title + Serial + App Version + Category + Install Size + Play Time + Last Played + Calculating\u2026 + Unknown + %1$d KB + %1$.1f MB + %1$.2f GB + Never played + %1$dh %2$dm + %1$dm %2$ds + %1$ds + Never + + + Build: %1$s + Revision: %1$s + Vita3K does not condone piracy. You must dump your own games. + + + Check for Updates + Checking for updates… + An update check is already running. + Update Available + Official Build Available + Latest build + Open Download Page + A newer Vita3K build is available.\n\nCurrent version: %1$s\nLatest version: %2$s + A newer Vita3K build is available.\n\nCurrent version: %1$s\nLatest version: %2$s\nPublished: %3$s + You are currently running a non-official or PR build.\n\nIf you update, you will switch to the latest official build and will no longer be on this custom or PR build.\n\nLatest official build: %1$s + You are currently running a non-official or PR build.\n\nIf you update, you will switch to the latest official build and will no longer be on this custom or PR build.\n\nLatest official build: %1$s\nPublished: %2$s + + + Welcome to Vita3K + Vita3K is an open-source PlayStation Vita emulator written in C++ for Windows, Linux, macOS, and Android. The emulator is still under active development, so feedback and testing are greatly appreciated. + Vita3K does not condone piracy. You must dump your own games. + Install Firmware + To get started, install the PS Vita firmware files below. + Pre-Install Package + Main Firmware + Font Package + Download + Install + Select Firmware Language + Installed + Missing + Optional + Back + Next + Skip for now + Open Library + + + Paused + In-game session + Close pause menu + Session + Controls + Controller + Settings + Resume + Pause + Exit + Exit Game? + Any unsaved in-game progress will be lost. + Custom Config + This game has a saved custom config. + This game is using global settings. + Create + Save + Reset + All Settings + Settings saved. + Show Controls + Show L2 / R2 + Show L3 / R3 + Touchscreen Switch + Hide Toggle + Overlay Scale + Overlay Opacity + Edit Layout + Done + Reset Layout + Edit Controls + Some settings take effect after restarting the game. + Restart Required + Some changes need an app restart to fully take effect. + These changed settings will apply after restart:\n- %1$s + Restart App + Restart Later + Restarting App + Please wait while Vita3K restarts the game. + Restart Game + Continue Playing + Start typing… + + + Install Content + Firmware (PUP) + Install PS Vita firmware + Package (PKG) + Install a .pkg app or DLC + Archive (ZIP/VPK) + Install from .zip or .vpk file + License + Install a .rif or .bin license file + Installing + Install Successful + Install Complete + Install Failed + Install progress + Keeps content installs running while Vita3K is backgrounded. + Install Archive + Choose a single archive file or a folder containing multiple .zip and .vpk archives. + Select archive file + Select archive folder + Install License + Choose a license file or enter a zRIF key manually. + Delete firmware file after install + Delete package file after install + Delete archive files after install + Delete license file after install + + + Enter zRIF Key + zRIF + License Required + No license was found automatically.\nHow would you like to provide it? + Select .bin / .rif file + Enter zRIF key manually + + + Installing firmware\u2026 + Preparing\u2026 + Installing package\u2026 + Installing archive\u2026 + Installing archive %1$d of %2$d: %3$s\u2026 + Installing license\u2026 + Firmware %1$s installed successfully. + Firmware installation failed. + Package installed successfully. + Package installation failed. + Archive installed successfully. + Archive installation failed. + %1$d archive(s) installed successfully. + Archive install complete. %1$d succeeded, %2$d failed. + Archive installation failed for %1$d file(s). + License installed successfully. + License installation failed. + Failed to create license from zRIF key. + The selected folder contains no .zip or .vpk archives. + Failed to read license file. + Internal error: PKG path lost. + Error: %1$s + + + Filter & View + Sort By + View Mode + List + Grid + Filter + + + Settings + Open Settings + Settings - %1$s + Using custom config for this app + Delete Custom Config + Clear All Custom Configs + Delete Custom Config? + This will remove the custom configuration for %1$s and revert to global settings. + Clear All Custom Configs? + This will remove all per-app custom configuration files. This action cannot be undone. + Cleared %1$d custom configs. + Unsaved Changes + You have unsaved changes. What would you like to do? + Discard + Reset to Defaults + Reset Settings to Defaults? + This resets the current settings form to the emulator defaults. Storage folder changes are not included until you change them separately. + Settings reset to defaults. + Save and Exit + Save settings + More options + Search settings\u2026 + Search Results + No matching settings. + Success + Global only + Per-app only + Enabled + Disabled + Show setting details + + + Core + CPU + Graphics + Audio + System + Controls + Interface + Network + Debug + Emulator + + + Module Loading Mode + Automatic + Automatically select modules to load. Recommended for most apps. + Auto & Manual + Select this mode to load modules automatically in addition to selected modules from the list on the right. + Manual + Only load the modules selected from the list. Advanced users only. + LLE Modules List + Search modules\u2026 + No modules available. Install firmware first. + Firmware Language + Pick the PlayStation firmware page locale to open in your browser. + Download Firmware + Module Management + + + Enable CPU Optimizations + Enable Dynarmic JIT optimizations. Improves performance. + Dynarmic Settings + + + Renderer + Image Quality + Textures and Shaders + Shaders + Hacks + Custom GPU Driver + Backend Renderer + Select the preferred backend renderer. Vulkan is recommended for most systems. + Graphics Device + Rendering Accuracy + Set the renderer accuracy level for Vulkan. High accuracy may improve visuals but reduce performance. + Standard + High + V-Sync + Enable V-Sync for OpenGL. Reduces screen tearing. + Disable Surface Sync + Speed hack, disabling turns off surface syncing between CPU and GPU.\nSurface syncing is needed by a few games.\nGives a big performance boost if disabled (in particular when upscaling is on). + Asynchronous Pipeline Compilation + Allow pipelines to be compiled concurrently on multiple concurrent threads.\nThis decreases pipeline compilation stutter at the cost of temporary graphical glitches. + Screen Filter + Select the final image filter to apply. + Internal Resolution Upscaling + Scale the games resolution by a multiplier.\nExperimental: apps are not guaranteed to render properly at more than 1x. + %1$d\u00D7%2$d (%3$s\u00D7) + Anisotropic Filtering + Technique to increase the sharpness of textures which are sloped relative to the viewer.\nIt has no drawbacks but can impact performance on older GPUs. + Textures + Export Textures + Export textures used by the app to the textures folder. + Import Textures + Import replacement textures from the textures folder. + Texture Exporting Format + Format to export textures in + FPS Hack + Game hack. May double the framerate from 30 FPS to 60 FPS in some games,\nbut can cause some games to run twice as fast. + Turbo Mode + Provides a way to force the GPU to run at the maximum possible clocks. Thermal constraints will still be applied. + Enable Shader Cache + Enable shader caching. Reduces stuttering on subsequent runs. + Use SPIR-V Shader (deprecated) + Pass generated Spir-V shader directly to driver.\nNote that some beneficial extensions will be disabled, and not all GPUs are compatible with this. + Memory Mapping + Memory mapping improved performance, reduces memory usage and fixes many graphical issues. However, it may be unstable on some GPUs. + Custom GPU Driver + Choose which installed custom GPU driver to inject. Installing drivers requires a compatible Android GPU driver ZIP and a supported Qualcomm Vulkan stack. + Default + %1$s (missing) + The selected custom driver is no longer installed. Choose Default, reinstall it, or select another installed driver before launching an app. + The selected custom driver is loaded and active. + The selected custom driver could not be loaded, so Vita3K is using the default system driver instead. + Download Drivers + Install Driver + Remove Selected Driver + No memory mapping support for current device. + Failed to resolve custom driver archive path. + Installed and loaded custom driver %1$s. + Installed custom driver %1$s, but it could not be loaded. Vita3K fell back to the default system driver. + Failed to install the selected custom driver archive. + Loaded custom driver %1$s successfully. + Failed to load custom driver %1$s. Vita3K fell back to the default system driver. + Removed custom driver %1$s. + Failed to remove custom driver %1$s. + Remove Custom Driver? + This will permanently remove the installed custom driver %1$s from device storage. + + + Audio Output + Features + Audio Settings + Audio Backend + Select the audio backend.\nCubeb is recommended for most systems. + Volume + Set the in-game audio volume. + Current volume: %1$d%% + Enable NGS Support + Enable advanced audio library NGS support. + + + Camera + Camera Sources + Front Camera + Select the front camera source. Solid Color or Static Image can be used as substitutes. + Back Camera + Select the back camera source. Solid Color or Static Image can be used as substitutes. + Camera Source + Solid Color + Static Image + Camera Color + Choose the fallback color used when the camera source is Solid Color. + Camera Image + Pick the fallback image used when the camera source is Static Image. + No camera devices detected. + No image selected + Failed to resolve the selected camera image path. + + + Console Settings + System Modes + Region and Language + System Settings + Enter Button Assignment + Select which button acts as the Enter/Confirm button. Some apps ignore this setting. + Circle + Cross + PlayStation TV Mode (PSTV) + Enable PlayStation TV mode. + Show Mode + Enable Show Mode. + Demo Mode + Enable Demo Mode. + System Language + Set the system language reported to applications. + Date Format + Set the date format used by the emulated system. + Time Format + Set the time format used by the emulated system. + IME Languages + Select which IME keyboard languages are available to applications. + + + Connection + PlayStation Network + HTTP Networking + Timeout and Retry + PSN Signed In + Pretend to be signed in to PlayStation Network (but offline). + Enable HTTP Networking + Enable HTTP networking support for apps that use it. + Timeout Attempts + How many times to retry connecting when the server doesn\'t respond. Could be useful if you have very unstable or VERY SLOW internet. + Timeout Sleep + How long to wait between connection retries. Could be useful if you have very unstable or VERY SLOW internet. + Read End Attempts + How many retries to attempt when there isn\'t more data to read, lower can improve performance but can make games unstable if you have bad enough internet. + Read End Sleep + How long to wait between read attempt retries, lower can improve performance but can make games unstable if you have bad enough internet + %1$d attempts + %1$d ms + Ad-Hoc Address + Select the IP address to be used in adhoc networking. + Subnet Mask: %1$s + + + Show On-Screen Controls + Show the touch control overlay while a game is running. + Hide Overlay When Controller Is Connected + Automatically hide the touch overlay whenever a physical controller is connected. + Show L2 / R2 Buttons + Add the extra trigger buttons to the touch overlay. + Show L3 / R3 Buttons + Add the stick-click buttons to the touch overlay. + Show Touchscreen Switch + Show the button that switches between front and rear touch input. + Show Hide Toggle + Show the button that temporarily hides the on-screen controls. + Overlay Scale + Resize the touch controls to better fit your hands and screen. + Overlay Opacity + Adjust how transparent the touch controls appear over the game. + %1$d%% + Controller connected + No controller connected + + + Diagnostics + Logging + Graphics + Miscellaneous + Connected Controllers + Shows the physical controllers Android currently reports to the frontend. Button labels follow the first connected controller when possible. + No physical controller detected. + You can still choose mappings manually even if no controller is currently connected. + Controller Button Mapping + Choose which physical controller button is used for each input. + Tap a row, then press a controller button or choose one manually from the list. + Stick and Trigger Mapping + Choose which controller axes drive the analog sticks and PSTV triggers. + Tap a row, then move a stick or trigger or choose an axis manually from the list. + Up and Down share one vertical axis. Left and Right share one horizontal axis. + Controller Behavior + Analog Stick Multiplier + Scale analog stick input before it reaches the emulator. Higher values make sticks feel more aggressive. + %1$.1fx + Disable Motion Controls + Ignore controller and device motion sensors for games that expect gyro or accelerometer input. + Press a controller button now, or choose a mapping from the list below. + Move a controller stick or trigger now, or choose a mapping from the list below. + Listening for controller input... + Connect a controller to use live capture. + Manual Selection + D-Pad Up + D-Pad Down + D-Pad Left + D-Pad Right + Triangle + Circle + Cross + Square + L1 + R1 + L2 + R2 + L3 + R3 + Select + Start + PS Button + Left Stick Up + Left Stick Down + Left Stick Left + Left Stick Right + Right Stick Up + Right Stick Down + Right Stick Left + Right Stick Right + Import Logging + Log module import symbols. + Export Logging + Log module export symbols. + Log Active Shaders + Log active shader programs. + Log Shader Uniforms + Log shader uniform values. + Save color surfaces + Save color surfaces to files. + ELF Dumping + Dump loaded code as ELFs. + Vulkan Validation Layer + Enable Vulkan validation layers.\nUseful for debugging but reduces performance. + + + Appearance + UI Options + Behavior + General + Logging + Display and Overlay + Storage and Capture + Enable Texture Cache + Enable the texture cache. Improves performance in games at the cost of additional VRAM usage. + Interface Language + Select the interface language used by the Android frontend.\nUse System Default to follow the operating system language. + Emulated System Storage Folder + Change the location of the emulated system storage folder.\nIf you move to a different folder, move your existing data there manually. + Share vita3k.log + Open Android’s share sheet for the current Vita3K log file so it can be sent to Discord or another app. + Failed to resolve the selected storage folder. + Failed to switch the emulator storage folder. + Switched emulator storage folder to %1$s. + Stretch the Display Area + Stretch the display area to fill the entire window, ignoring the original aspect ratio. + Fullscreen HD Pixel Perfect + Check the box to get a pixel perfect rendering with HD resolutions (1080p, 4K) in fullscreen on a 16:9 aspect ratio monitor at the price of slight cropping at the top and the bottom of the screen. + File Loading Delay: %1$d ms + Add an artificial delay to file loading. This is required for some games that load files too quickly compared to real hardware (e.g., Silent Hill). + Show Shader Compilation Hint + Show a hint when shaders are being compiled during gameplay. + Update Check Mode + Choose whether Vita3K checks for updates at startup.\n"Check" notifies you when a newer build is available, and "Don\'t Check" disables startup checks. + Check + Don\'t Check + Archive Log + Keep a duplicate of the log file named after the current title when an app runs. + Log Compatibility Warnings + Log compatibility database related warnings. + Log Level + Select the verbosity of the logging system. + Performance Overlay + Show a performance overlay with FPS and other statistics. + Overlay Detail + Select the detail level for the performance overlay. + Overlay Position + Select the location to display the performance overlay in. + Screenshot Format + Select image format to select screenshots in. + + + Trace + Debug + Info + Warning + Error + Critical + Off + Minimum + Low + Medium + Maximum + Top Left + Top Center + Top Right + Bottom Left + Bottom Center + Bottom Right + None + 12-Hour + 24-Hour + YYYY/MM/DD + DD/MM/YYYY + MM/DD/YYYY + + + Japanese + English (US) + French + Spanish + German + Italian + Dutch + Portuguese (PT) + Russian + Korean + Chinese (Traditional) + Chinese (Simplified) + Finnish + Swedish + Danish + Norwegian + Polish + Portuguese (BR) + English (GB) + Turkish + + + Danish + German + English (US) + Spanish + French + Italian + Dutch + Norwegian + Polish + Portuguese (PT) + Russian + Finnish + Swedish + Japanese + Korean + Simplified Chinese + Traditional Chinese + Portuguese (BR) + English (GB) + Turkish + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..a1d4c3642 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/android/app/src/main/res/xml/locales_config.xml b/android/app/src/main/res/xml/locales_config.xml new file mode 100644 index 000000000..43b283618 --- /dev/null +++ b/android/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle index bcb57ad51..f440db12e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,112 +1,10 @@ -apply plugin: 'com.android.application' - -android { - namespace "org.vita3k.emulator" - compileSdk 35 - ndkVersion "27.3.13750724" - buildFeatures { - buildConfig true - } - - defaultConfig { - applicationId "org.vita3k.emulator" - minSdk 28 - targetSdk 35 - versionCode 21 - versionName "0.2.1" - - externalNativeBuild { - cmake { - abiFilters.addAll( 'arm64-v8a' ) - arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" // required until NDK r28 - } - } - } - - signingConfigs { - ci { - storeFile file(System.getenv("SIGNING_STORE_PATH") ?: "${System.getenv("user.home")}/keystore.jks") - storePassword System.getenv("SIGNING_STORE_PASSWORD") - keyAlias System.getenv("SIGNING_KEY_ALIAS") - keyPassword System.getenv("SIGNING_KEY_PASSWORD") - } - } - - buildTypes { - debug { - debuggable true - jniDebuggable true - applicationIdSuffix '.debug' - } - - reldebug { - debuggable true - jniDebuggable true - externalNativeBuild { - cmake { - arguments "-DCMAKE_BUILD_TYPE=RelWithDebInfo" - } - } - minifyEnabled false - shrinkResources false - applicationIdSuffix '.debug' - signingConfig = signingConfigs.debug - } - - release { - debuggable false - jniDebuggable false - externalNativeBuild { - cmake { - arguments "-DCMAKE_BUILD_TYPE=Release" - } - } - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - signingConfig = signingConfigs.ci - } - } - - sourceSets { - main { - jniLibs.srcDirs = ['prebuilt'] - assets { - srcDirs 'assets' - } - java { - srcDirs += ['../external/sdl/android-project/app/src/main/java'] - } - } - } - - externalNativeBuild { - cmake { - path '../CMakeLists.txt' - version '3.22.1+' - } - } - - buildFeatures { - viewBinding true - } - lint { - abortOnError false - checkReleaseBuilds false - } - packagingOptions { - jniLibs { - useLegacyPackaging true - } - } +plugins { + id 'com.android.application' version '8.13.0' apply false + id 'com.android.library' version '8.13.0' apply false + id 'org.jetbrains.kotlin.android' version '2.1.20' apply false + id 'org.jetbrains.kotlin.plugin.compose' version '2.1.20' apply false } -dependencies { - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation "androidx.core:core:1.12.0" - implementation 'androidx.core:core-google-shortcuts:1.1.0' - implementation 'com.google.android.material:material:1.11.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'com.getkeepsafe.relinker:relinker:1.4.5' - implementation 'com.jakewharton:process-phoenix:2.1.2' +task clean(type: Delete) { + delete rootProject.buildDir } diff --git a/gradle.properties b/android/gradle.properties similarity index 100% rename from gradle.properties rename to android/gradle.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to android/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to android/gradle/wrapper/gradle-wrapper.properties diff --git a/gradlew b/android/gradlew old mode 100755 new mode 100644 similarity index 100% rename from gradlew rename to android/gradlew diff --git a/gradlew.bat b/android/gradlew.bat similarity index 100% rename from gradlew.bat rename to android/gradlew.bat diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro deleted file mode 100644 index 248af752a..000000000 --- a/android/proguard-rules.pro +++ /dev/null @@ -1,2 +0,0 @@ -# this is necessary so that native functions can call Java ones using JNI --keep class org.libsdl.app** { *; } diff --git a/settings.gradle b/android/settings.gradle similarity index 93% rename from settings.gradle rename to android/settings.gradle index 6e172d123..932e79cb2 100644 --- a/settings.gradle +++ b/android/settings.gradle @@ -13,4 +13,4 @@ dependencyResolutionManagement { } } rootProject.name = "Vita3K" -include ':android' +include ':app' diff --git a/android/src/main/java/org/vita3k/emulator/Emulator.java b/android/src/main/java/org/vita3k/emulator/Emulator.java deleted file mode 100644 index a26ad1bf9..000000000 --- a/android/src/main/java/org/vita3k/emulator/Emulator.java +++ /dev/null @@ -1,348 +0,0 @@ -package org.vita3k.emulator; - -import android.content.Context; -import android.content.Intent; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.provider.Settings; -import android.system.ErrnoException; -import android.system.Os; -import android.view.Surface; -import android.view.ViewGroup; -import android.view.View; -import android.widget.Toast; - -import androidx.annotation.Keep; -import androidx.core.content.FileProvider; -import androidx.core.content.pm.ShortcutInfoCompat; -import androidx.core.content.pm.ShortcutManagerCompat; -import androidx.core.graphics.drawable.IconCompat; -import androidx.documentfile.provider.DocumentFile; - -import com.jakewharton.processphoenix.ProcessPhoenix; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; - -import org.libsdl.app.SDLActivity; -import org.libsdl.app.SDLSurface; -import org.vita3k.emulator.overlay.InputOverlay; - -public class Emulator extends SDLActivity -{ - private String currentGameId = ""; - private EmuSurface mSurface; - - public InputOverlay getmOverlay() { - return mSurface.getmOverlay(); - } - - @Keep - public void setCurrentGameId(String gameId){ - currentGameId = gameId; - } - - /** - * This method is called by SDL before loading the native shared libraries. - * It can be overridden to provide names of shared libraries to be loaded. - * The default implementation returns the defaults. It never returns null. - * An array returned by a new implementation must at least contain "SDL2". - * Also keep in mind that the order the libraries are loaded may matter. - * - * @return names of shared libraries to be loaded (e.g. "SDL3", "main"). - */ - @Override - protected String[] getLibraries() { - return new String[] { "Vita3K" }; - } - - @Override - protected SDLSurface createSDLSurface(Context context) { - mSurface = new EmuSurface(context); - mSurface.post(() -> { - if (mSurface.getParent() instanceof ViewGroup) { - ((ViewGroup) mSurface.getParent()).addView(getmOverlay()); - } - }); - return mSurface; - } - - static private final String APP_RESTART_PARAMETERS = "AppStartParameters"; - - @Override - protected String[] getArguments() { - Intent intent = getIntent(); - - String[] args = intent.getStringArrayExtra(APP_RESTART_PARAMETERS); - if(args == null) - args = new String[]{}; - - return args; - } - - @Override - protected void onNewIntent(Intent intent){ - super.onNewIntent(intent); - - // if we start the app from a shortcut and are in the main menu - // or in a different game, start the new game - if(intent.getAction().startsWith("LAUNCH_")){ - String game_id = intent.getAction().substring(7); - if(!game_id.equals(currentGameId)) - ProcessPhoenix.triggerRebirth(getContext(), intent); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - hideSystemBars(); - } - - @Override - protected void onResume() { - super.onResume(); - hideSystemBars(); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - if (hasFocus) { - hideSystemBars(); - } - } - - @Keep - public void restartApp(String app_path, String exec_path, String exec_args){ - ArrayList args = new ArrayList<>(); - - // first build the args given to Vita3K when it restarts - // this is similar to run_execv in main.cpp - args.add("-a"); - args.add("true"); - if(!app_path.isEmpty()){ - args.add("-r"); - args.add(app_path); - - if(!exec_path.isEmpty()){ - args.add("--self"); - args.add(exec_path); - - if(!exec_args.isEmpty()){ - args.add("--app-args"); - args.add(exec_args); - } - } - } - - Intent restart_intent = new Intent(getContext(), Emulator.class); - restart_intent.putExtra(APP_RESTART_PARAMETERS, args.toArray(new String[]{})); - ProcessPhoenix.triggerRebirth(getContext(), restart_intent); - } - - static final int FILE_DIALOG_CODE = 545; - static final int FOLDER_DIALOG_CODE = 546; - - @Keep - public void setStoragePermission() { - Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)); - startActivity(intent); - } - - @Keep - public void showFileDialog() { - if (!isStorageManagerEnabled()) { - setStoragePermission(); - return; - } - - Intent intent = new Intent() - .setType("*/*") - .setAction(Intent.ACTION_GET_CONTENT) - .putExtra(Intent.EXTRA_LOCAL_ONLY, true); - - intent = Intent.createChooser(intent, "Choose a file"); - startActivityForResult(intent, FILE_DIALOG_CODE); - } - - @Keep - public boolean isStorageManagerEnabled(){ - // If running Android 10-, SDL should have already asked for read and write permissions - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) - return true; - - return Environment.isExternalStorageManager(); - } - - @Keep - public void showFolderDialog() { - if (!isStorageManagerEnabled()) { - setStoragePermission(); - return; - } - - Intent intent = new Intent() - .setAction(Intent.ACTION_OPEN_DOCUMENT_TREE) - .putExtra(Intent.EXTRA_LOCAL_ONLY, true); - - intent = Intent.createChooser(intent, "Choose a folder"); - startActivityForResult(intent, FOLDER_DIALOG_CODE); - } - - private String resolveUriToPath(Uri result_uri) { - String result_path = ""; - - try (ParcelFileDescriptor file_descr = getContentResolver().openFileDescriptor(result_uri, "r")) { - result_path = Os.readlink("/proc/self/fd/" + file_descr.getFd()); - - // replace /mnt/user/{id} with /storage - if (result_path.startsWith("/mnt/user/")) { - result_path = result_path.substring("/mnt/user/".length()); - result_path = "/storage" + result_path.substring(result_path.indexOf('/')); - } - - } catch (Exception e) { - } - - return result_path; - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - switch (requestCode) { - // --- PICKER --- - case FILE_DIALOG_CODE: - case FOLDER_DIALOG_CODE: { - if (resultCode != RESULT_OK) { - filedialogReturn(""); - break; - } - // get uri from result - Uri result_uri = data.getData(); - - // for folder picker, convert to tree uri - if (requestCode == FOLDER_DIALOG_CODE) - result_uri = DocumentFile.fromTreeUri(getApplicationContext(), result_uri).getUri(); - - // resolve uri to path - String res = resolveUriToPath(result_uri); - filedialogReturn(res != null ? res : ""); - break; - } - } - } - - @Keep - public void setControllerOverlayState(int overlay_mask, boolean edit, boolean reset){ - getmOverlay().setState(overlay_mask); - getmOverlay().setIsInEditMode(edit); - - if(reset) - getmOverlay().resetButtonPlacement(); - } - - @Keep - public void setControllerOverlayScale(float scale){ - getmOverlay().setScale(scale); - } - - @Keep - public void setControllerOverlayOpacity(int opacity){ - getmOverlay().setOpacity(opacity); - } - - @Keep - public boolean createShortcut(String game_id, String game_name){ - if(!ShortcutManagerCompat.isRequestPinShortcutSupported(getContext())) - return false; - - // first look at the icon, its location should always be the same - File src_icon = new File(getExternalFilesDir(null), "cache/icons/" + game_id + ".png"); - Bitmap icon; - if(src_icon.exists()) - icon = BitmapFactory.decodeFile(src_icon.getPath()); - else - icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); - - // intent to directly start the game - Intent game_intent = new Intent(getContext(), Emulator.class); - ArrayList args = new ArrayList(); - args.add("-r"); - args.add(game_id); - game_intent.putExtra(APP_RESTART_PARAMETERS, args.toArray(new String[]{})); - game_intent.setAction("LAUNCH_" + game_id); - - // now create the pinned shortcut - ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(getContext(), game_id) - .setShortLabel(game_name) - .setLongLabel(game_name) - .setIcon(IconCompat.createWithBitmap(icon)) - .setIntent(game_intent) - .build(); - ShortcutManagerCompat.requestPinShortcut(getContext(), shortcut, null); - - return true; - } - - @Keep - public void requestInstallUpdate() { - runOnUiThread(() -> { - File apkFile = new File(getExternalFilesDir(null), "vita3k-latest.apk"); - - if (!apkFile.exists()) { - Toast.makeText(this, "APK file not found.", Toast.LENGTH_LONG).show(); - return; - } - - try { - Uri apkUri = FileProvider.getUriForFile( - this, - BuildConfig.APPLICATION_ID + ".fileprovider", - apkFile - ); - - Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); - intent.setData(apkUri); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK); - - startActivity(intent); - } catch (Exception e) { - e.printStackTrace(); - Toast.makeText(this, "Failed to launch installer.", Toast.LENGTH_LONG).show(); - } - }); - } - - @Keep - public int getNativeDisplayRotation() { - // Returns the device's default display rotation (0, 90, 180, or 270 degrees) - return getWindowManager().getDefaultDisplay().getRotation(); - } - - public native void filedialogReturn(String result_path); - - private void hideSystemBars() { - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - ); - } -} diff --git a/android/src/main/java/org/vita3k/emulator/provider/VitaDocumentsProvider.java b/android/src/main/java/org/vita3k/emulator/provider/VitaDocumentsProvider.java deleted file mode 100644 index f472688a5..000000000 --- a/android/src/main/java/org/vita3k/emulator/provider/VitaDocumentsProvider.java +++ /dev/null @@ -1,204 +0,0 @@ -package org.vita3k.emulator.provider; - -import android.database.Cursor; -import android.database.MatrixCursor; -import android.os.CancellationSignal; -import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract; -import android.provider.DocumentsProvider; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import androidx.annotation.Nullable; - -import org.vita3k.emulator.R; - -import java.io.File; -import java.io.FileNotFoundException; - -// Strongly inspired by https://github.com/libretro/RetroArch/blob/master/pkg/android/phoenix/src/com/retroarch/browser/provider/RetroDocumentsProvider.java -public class VitaDocumentsProvider extends DocumentsProvider { - - private final String root = "VitaRoot"; - - private final String[] DEFAULT_ROOT_PROJECTION = new String[]{ - DocumentsContract.Root.COLUMN_ROOT_ID, - DocumentsContract.Root.COLUMN_MIME_TYPES, - DocumentsContract.Root.COLUMN_FLAGS, - DocumentsContract.Root.COLUMN_TITLE, - DocumentsContract.Root.COLUMN_DOCUMENT_ID, - DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, - DocumentsContract.Root.COLUMN_ICON - }; - - private final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{ - DocumentsContract.Document.COLUMN_DOCUMENT_ID, - DocumentsContract.Document.COLUMN_MIME_TYPE, - DocumentsContract.Document.COLUMN_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_LAST_MODIFIED, - DocumentsContract.Document.COLUMN_FLAGS, - DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_ICON - }; - - @Override - public boolean onCreate() { - return true; - } - - private static String getMimeType(File file) { - if (file.isDirectory()) { - return DocumentsContract.Document.MIME_TYPE_DIR; - } else { - final String name = file.getName(); - final int lastDot = name.lastIndexOf('.'); - if (lastDot >= 0) { - final String extension = name.substring(lastDot + 1).toLowerCase(); - final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - if (mime != null) return mime; - } - return "application/octet-stream"; - } - } - - private File getStorageDir(){ - File storage_dir = new File(getContext().getExternalFilesDir(null), "vita"); - if(!storage_dir.exists()) - storage_dir.mkdirs(); - - return storage_dir; - } - - private File resolveFile(String documentId) throws FileNotFoundException { - if(documentId.startsWith(root)){ - File storage_dir = getStorageDir(); - - storage_dir = new File(storage_dir, documentId.substring(root.length() + 1)); - if(!storage_dir.exists()) - throw new FileNotFoundException(documentId + " was not found"); - - return storage_dir; - } - - throw new FileNotFoundException(documentId + " was not found"); - } - - private String getDocumentId(File file) { - return root + ":" + file.getAbsolutePath().substring(getStorageDir().getAbsolutePath().length()); - } - - private void applyCursor(MatrixCursor cursor, File file, String documentId) throws FileNotFoundException { - if(file == null) - file = resolveFile(documentId); - if(documentId == null) - documentId = getDocumentId(file); - - boolean is_root = file == getStorageDir(); - int flags = 0; - if(file.isDirectory()){ - flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE - | DocumentsContract.Document.FLAG_SUPPORTS_DELETE - | DocumentsContract.Document.FLAG_SUPPORTS_REMOVE - | DocumentsContract.Document.FLAG_SUPPORTS_RENAME; - } else { - flags = DocumentsContract.Document.FLAG_SUPPORTS_COPY - | DocumentsContract.Document.FLAG_SUPPORTS_DELETE - | DocumentsContract.Document.FLAG_SUPPORTS_MOVE - | DocumentsContract.Document.FLAG_SUPPORTS_REMOVE - | DocumentsContract.Document.FLAG_SUPPORTS_RENAME - | DocumentsContract.Document.FLAG_SUPPORTS_WRITE; - } - - MatrixCursor.RowBuilder row = cursor.newRow(); - row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId) - .add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, is_root ? "Vita3K" : file.getName()) - .add(DocumentsContract.Document.COLUMN_SIZE, file.length()) - .add(DocumentsContract.Document.COLUMN_MIME_TYPE, getMimeType(file)) - .add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified()) - .add(DocumentsContract.Document.COLUMN_FLAGS, flags); - if(is_root) - row.add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher); - } - - @Override - public Cursor queryRoots(String[] projection) throws FileNotFoundException { - MatrixCursor cursor = new MatrixCursor(projection == null ? DEFAULT_ROOT_PROJECTION : projection); - File storage_dir = getStorageDir(); - - cursor.newRow() - .add(DocumentsContract.Root.COLUMN_ROOT_ID, root) - .add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE | DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD) - .add(DocumentsContract.Root.COLUMN_TITLE, "Vita3K") - .add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(storage_dir)) - .add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*") - .add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, storage_dir.getFreeSpace()) - .add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher); - - return cursor; - } - - @Override - public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { - MatrixCursor cursor = new MatrixCursor(projection == null ? DEFAULT_DOCUMENT_PROJECTION : projection); - applyCursor(cursor, null, documentId); - return cursor; - } - - @Override - public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { - MatrixCursor cursor = new MatrixCursor(projection == null ? DEFAULT_DOCUMENT_PROJECTION : projection); - File folder = resolveFile(parentDocumentId); - for(File child : folder.listFiles()) - applyCursor(cursor, child, null); - return cursor; - } - - @Override - public ParcelFileDescriptor openDocument(String documentId, String mode, @Nullable CancellationSignal signal) throws FileNotFoundException { - File file = resolveFile(documentId); - int access_mode = ParcelFileDescriptor.parseMode(mode); - return ParcelFileDescriptor.open(file, access_mode); - } - - @Override - public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { - File parent = resolveFile(parentDocumentId); - int nb_conflicts = 1; - File new_file = new File(parent, displayName); - while(new_file.exists()) - new_file = new File(parent, displayName + " (" + (++nb_conflicts) + ")"); - - try { - boolean success = false; - if (mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR)) - success = new_file.mkdir(); - else - success = new_file.createNewFile(); - - if(!success) - throw new FileNotFoundException(); - } catch (Exception e){ - throw new FileNotFoundException(); - } - - return getDocumentId(new_file); - } - - @Override - public void deleteDocument(String documentId) throws FileNotFoundException { - File file = resolveFile(documentId); - if(!file.delete()) - throw new FileNotFoundException(); - } - - @Override - public String getDocumentType(String documentId) throws FileNotFoundException { - File file = resolveFile(documentId); - return getMimeType(file); - } - - @Override - public boolean isChildDocument(String parentDocumentId, String documentId) { - return documentId.startsWith(parentDocumentId); - } -} diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml deleted file mode 100644 index 3ab3e9cbc..000000000 --- a/android/src/main/res/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #3F51B5 - #303F9F - #FF4081 - diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml deleted file mode 100644 index 7da9b032f..000000000 --- a/android/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Vita3K - diff --git a/android/src/main/res/values/styles.xml b/android/src/main/res/values/styles.xml deleted file mode 100644 index 18f919d6c..000000000 --- a/android/src/main/res/values/styles.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/android/src/main/res/xml/file_paths.xml b/android/src/main/res/xml/file_paths.xml deleted file mode 100644 index 7e44e657b..000000000 --- a/android/src/main/res/xml/file_paths.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/appimage/build_updater.sh b/appimage/build_updater.sh deleted file mode 100755 index 9e746b4af..000000000 --- a/appimage/build_updater.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -# Note: The documentation at https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/blob/master/README.md for embedding update info is wrong. -# Detect architecture -# The correct env-variable name to use is UPDATE_INFORMATION, and *not* LDAI_UPDATE_INFORMATION. - -# Detect architecture -ARCH=$(uname -m) - -case "$ARCH" in - x86_64|amd64) APPIMAGE_NAME="Vita3K-x86_64.AppImage" ;; - aarch64|arm64) APPIMAGE_NAME="Vita3K-aarch64.AppImage" ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; -esac - -echo "Executing linuxdeploy with cmdline: LDAI_VERBOSE=1 UPDATE_INFORMATION=\"gh-releases-zsync|Vita3K|Vita3K|latest|${APPIMAGE_NAME}.zsync\" $@" - -LDAI_VERBOSE=1 UPDATE_INFORMATION="gh-releases-zsync|Vita3K|Vita3K|latest|${APPIMAGE_NAME}.zsync" $@ diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 705ab6e41..000000000 --- a/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - id 'com.android.application' version '8.13.0' apply false - id 'com.android.library' version '8.13.0' apply false -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/cmake/qt6.cmake b/cmake/qt6.cmake new file mode 100644 index 000000000..148b81670 --- /dev/null +++ b/cmake/qt6.cmake @@ -0,0 +1,71 @@ +# Qt6 discovery and configuration for Vita3K +# Provides the vita3k::qt6 INTERFACE target. +add_library(vita3k_qt6 INTERFACE) + +set(VITA3K_QT_MIN_VER 6.7.0) + +set(VITA3K_QT_COMPONENTS Core Gui Widgets Network Concurrent Svg LinguistTools) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND VITA3K_QT_COMPONENTS GuiPrivate) +endif() + +find_package(Qt6 ${VITA3K_QT_MIN_VER} CONFIG COMPONENTS ${VITA3K_QT_COMPONENTS}) + +if(Qt6Widgets_FOUND) + if(Qt6Widgets_VERSION VERSION_LESS ${VITA3K_QT_MIN_VER}) + message(FATAL_ERROR "Minimum supported Qt version is ${VITA3K_QT_MIN_VER}! " + "You have version ${Qt6Widgets_VERSION}, please upgrade.") + endif() +else() + message("CMake was unable to find Qt6!") + if(WIN32) + message(FATAL_ERROR "Make sure the Qt6_ROOT environment variable has been set properly.\n" + "Example: Qt6_ROOT=C:\\Qt\\${VITA3K_QT_MIN_VER}\\msvc2022_64\\") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "Make sure to install your distro's Qt6 development packages!") + elseif(APPLE) + message(FATAL_ERROR "Make sure to install Qt6 development packages.\n" + "Set Qt6_ROOT to the installation path if CMake cannot find it.") + else() + message(FATAL_ERROR "You need Qt6 ${VITA3K_QT_MIN_VER} or later installed.\n" + "Visit https://www.qt.io/download-open-source/ for instructions.") + endif() +endif() + +target_link_libraries(vita3k_qt6 INTERFACE + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Network + Qt6::Concurrent + Qt6::Svg +) + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND TARGET Qt6::GuiPrivate) + target_link_libraries(vita3k_qt6 INTERFACE Qt6::GuiPrivate) +endif() + +if(WIN32) + target_compile_definitions(vita3k_qt6 INTERFACE WIN32_LEAN_AND_MEAN) +endif() + +add_library(vita3k::qt6 ALIAS vita3k_qt6) + +get_target_property(_qt6_qmake_executable Qt6::qmake IMPORTED_LOCATION) +get_filename_component(_qt6_bin_dir "${_qt6_qmake_executable}" DIRECTORY) + +if(WIN32) + find_program(WINDEPLOYQT_EXECUTABLE NAMES windeployqt6 windeployqt HINTS "${_qt6_bin_dir}" NO_DEFAULT_PATH) + if(WINDEPLOYQT_EXECUTABLE) + message(STATUS "Found windeployqt: ${WINDEPLOYQT_EXECUTABLE}") + else() + message(WARNING "windeployqt not found.") + endif() +elseif(APPLE) + find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt HINTS "${_qt6_bin_dir}" NO_DEFAULT_PATH) + if(MACDEPLOYQT_EXECUTABLE) + message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}") + else() + message(WARNING "macdeployqt not found.") + endif() +endif() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 4404d3146..3dca39915 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -84,12 +84,6 @@ target_include_directories(concurrentqueue INTERFACE "${CMAKE_CURRENT_SOURCE_DIR add_subdirectory(libfat16) -# The imgui target is including both imgui and imgui_club. -add_library(imgui STATIC imgui/imgui.cpp imgui/imgui_draw.cpp imgui/imgui_tables.cpp imgui/imgui_widgets.cpp imgui/misc/cpp/imgui_stdlib.cpp) -target_compile_definitions(imgui PRIVATE IMGUI_DISABLE_DEMO_WINDOWS) -target_include_directories(imgui PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/imgui" - "${CMAKE_CURRENT_SOURCE_DIR}/imgui_club/imgui_memory_editor/") - add_library(miniz STATIC miniz/miniz.c miniz/miniz.h) target_include_directories(miniz PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/miniz") diff --git a/external/ffmpeg b/external/ffmpeg index ccce45ff0..02f4f2691 160000 --- a/external/ffmpeg +++ b/external/ffmpeg @@ -1 +1 @@ -Subproject commit ccce45ff00217d480c0dcf7dc75b4cb4a5341db4 +Subproject commit 02f4f2691b0efffff8923235baf146a87fc37263 diff --git a/external/imgui b/external/imgui deleted file mode 160000 index cb16568fc..000000000 --- a/external/imgui +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cb16568fca5297512ff6a8f3b877f461c4323fbe diff --git a/external/imgui_club b/external/imgui_club deleted file mode 160000 index 53a2df3dd..000000000 --- a/external/imgui_club +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 53a2df3dd1b19dd321beb0897a0d1b9f87e5429c diff --git a/i18n/lang/overlay_da.json b/i18n/lang/overlay_da.json new file mode 100644 index 000000000..b83ccfe8d --- /dev/null +++ b/i18n/lang/overlay_da.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Afbryd" + }, + "common.yes": { + "message": "Ja" + }, + "common.no": { + "message": "Nej" + }, + "common.delete": { + "message": "Slet" + }, + "common.please_wait": { + "message": "Vent venligst..." + }, + "common.an_error_occurred": { + "message": "Der er opstået en fejl.\nFejlkode: {}" + }, + "common.could_not_load": { + "message": "Kunne ikke indlæse filen." + }, + "common.could_not_save": { + "message": "Kunne ikke gemme filen." + }, + "common.file_corrupted": { + "message": "Filen er defekt." + }, + "common.microphone_disabled": { + "message": "Aktivér mikrofonen." + }, + "dialog.save_data.save.title": { + "message": "Gem" + }, + "dialog.save_data.load.title": { + "message": "Indlæs" + }, + "dialog.save_data.save.saving": { + "message": "Gemmer..." + }, + "dialog.save_data.load.loading": { + "message": "Indlæser..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Lagring udført." + }, + "dialog.save_data.load.load_complete": { + "message": "Indlæsning udført." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Sletning udført." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nye gemte data" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Der er ingen gemte data." + }, + "dialog.save_data.save.save_the_data": { + "message": "Ønsker du at gemme dataene?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Ønsker du at indlæse disse gemte data?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Ønsker du at slette disse gemte data?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Ønsker du at overskrive disse gemte data?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Ønsker du at afbryde lagringen?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Ønsker du at afbryde indlæsningen?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Ønsker du at afbryde sletningen?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Gemmer...\nUndlad at slukke for systemet eller lukke programmet." + }, + "dialog.save_data.save.could_not_save": { + "message": "Kunne ikke gemme filen.\nDer er ikke nok ledig plads på hukommelseskortet. Du skal skabe mindst {} ledig plads for at gemme dit forløb i programmet.\n\nTryk på PS-knappen for at sætte programmet på pause, og slet derefter de andre programmer eller indhold for at skabe den ledige plads." + }, + "dialog.save_data.save.not_free_space": { + "message": "Der er ikke nok ledig plads på hukommelseskortet.\nDu skal skabe mindst {} ledig plads for at fortsætte med at bruge programmet.\n\nTryk på PS-knappen for at sætte programmet på pause, og slet derefter de andre programmer eller indhold." + }, + "dialog.save_data.info.details": { + "message": "Detaljer" + }, + "dialog.save_data.info.updated": { + "message": "Opdateret" + }, + "dialog.trophy.preparing_start_app": { + "message": "Forbereder start af programmet..." + }, + "overlay.trophy_earned": { + "message": "Du har opnået et trophy!" + } +} diff --git a/i18n/lang/overlay_de.json b/i18n/lang/overlay_de.json new file mode 100644 index 000000000..fbbaac97d --- /dev/null +++ b/i18n/lang/overlay_de.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Abbrechen" + }, + "common.yes": { + "message": "Ja" + }, + "common.no": { + "message": "Nein" + }, + "common.delete": { + "message": "Löschen" + }, + "common.please_wait": { + "message": "Warte bitte ..." + }, + "common.an_error_occurred": { + "message": "Ein Fehle ist aufgetreten.\nFehlercode: {}" + }, + "common.could_not_load": { + "message": "Laden der Datei gescheitert." + }, + "common.could_not_save": { + "message": "Speichern der Datei gescheitert." + }, + "common.file_corrupted": { + "message": "Die Datei ist beschädigt." + }, + "common.microphone_disabled": { + "message": "Aktiviere das Mikrofon." + }, + "dialog.save_data.save.title": { + "message": "Speichern" + }, + "dialog.save_data.load.title": { + "message": "Laden" + }, + "dialog.save_data.save.saving": { + "message": "Speichert ..." + }, + "dialog.save_data.load.loading": { + "message": "Lädt ..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Speichern abgeschlossen." + }, + "dialog.save_data.load.load_complete": { + "message": "Laden abgeschlossen." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Löschen abgeschlossen." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Neue gespeicherte Daten" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Es gibt keine gespeicherten Daten." + }, + "dialog.save_data.save.save_the_data": { + "message": "Möchtest du die Daten speichern?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Möchtest du diese gespeicherten Daten laden?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Möchtest du diese gespeicherten Daten löschen?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Möchtest du diese gespeicherten Daten überschreiben?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Möchtest du den Speichervorgang abbrechen?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Möchtest du den Ladevorgang abbrechen?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Möchtest du den Löschvorgang abbrechen?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Speichert ...\nSchalte das System nicht aus und schließe die Anwendung nicht." + }, + "dialog.save_data.save.could_not_save": { + "message": "Speichern der Datei gescheitert.\nNicht genügend freier Speicherplatz auf der Speicherkarte. Du musst mindestens {} freien Speicherplatz schaffen, damit du Fortschritte speichern kannst.\n\nUm freien Speicherplatz zu schaffen, drücke die PS-Taste, um diese Anwendung anzuhalten, und lösche dann andere Anwendungen oder Inhalte." + }, + "dialog.save_data.save.not_free_space": { + "message": "Nicht genügend freier Speicherplatz auf der Speicherkarte.\nUm die Anwendung weiterhin zu verwenden, müssen mindestens {} freier Speicherplatz geschaffen werden.\n\nDrücke die PS-Taste, um diese Anwendung anzuhalten, und lösche dann andere Anwendungen oder Inhalte." + }, + "dialog.save_data.info.details": { + "message": "Einzelheiten" + }, + "dialog.save_data.info.updated": { + "message": "Aktualisiert" + }, + "dialog.trophy.preparing_start_app": { + "message": "Starten der Anwendung wird vorbereitet ..." + }, + "overlay.trophy_earned": { + "message": "Du hast eine Trophäe freigeschaltet!" + } +} diff --git a/i18n/lang/overlay_en-GB.json b/i18n/lang/overlay_en-GB.json new file mode 100644 index 000000000..697c45298 --- /dev/null +++ b/i18n/lang/overlay_en-GB.json @@ -0,0 +1,110 @@ +{ + "common.ok": { + "message": "OK" + }, + "common.cancel": { + "message": "Cancel" + }, + "common.yes": { + "message": "Yes" + }, + "common.no": { + "message": "No" + }, + "common.delete": { + "message": "Delete" + }, + "common.submit": { + "message": "Submit" + }, + "common.please_wait": { + "message": "Please wait..." + }, + "common.an_error_occurred": { + "message": "An error occurred.\nError code: {}" + }, + "common.could_not_load": { + "message": "Could not load the file." + }, + "common.could_not_save": { + "message": "Could not save the file." + }, + "common.file_corrupted": { + "message": "The file is corrupt." + }, + "common.microphone_disabled": { + "message": "Enable the microphone." + }, + "dialog.save_data.save.title": { + "message": "Save" + }, + "dialog.save_data.load.title": { + "message": "Load" + }, + "dialog.save_data.save.saving": { + "message": "Saving..." + }, + "dialog.save_data.load.loading": { + "message": "Loading..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Saving complete." + }, + "dialog.save_data.load.load_complete": { + "message": "Loading complete." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Deletion complete." + }, + "dialog.save_data.save.new_saved_data": { + "message": "New Saved Data" + }, + "dialog.save_data.load.no_saved_data": { + "message": "There is no saved data." + }, + "dialog.save_data.save.save_the_data": { + "message": "Do you want to save the data?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Do you want to load this saved data?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Do you want to delete this saved data?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Do you want to overwrite this saved data?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Do you want to cancel saving?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Do you want to cancel loading?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Do you want to cancel deleting?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Saving...\nDo not power off the system or close the application." + }, + "dialog.save_data.save.could_not_save": { + "message": "Could not save the file.\nThere is not enough free space on the memory card. To save your progress in the application, you must create at least {} of free space.\n\nTo create the free space, press PS button to pause this application, and then delete other applications or content." + }, + "dialog.save_data.save.not_free_space": { + "message": "There is not enough free space on the memory card.\nTo continue using the application, you must create at least {} of free space.\n\nPress the PS button to pause this application, and then delete other applications or content." + }, + "dialog.save_data.info.details": { + "message": "Details" + }, + "dialog.save_data.info.updated": { + "message": "Updated" + }, + "dialog.trophy.preparing_start_app": { + "message": "Preparing to start the application..." + }, + "overlay.trophy_earned": { + "message": "You have earned a trophy!" + }, + "message.load_app_failed": { + "message": "Failed to load \"{}\".\nCheck vita3k.log to see console output for details.\n1. Have you installed the firmware?\n2. Re-dump your own PS Vita app/game and install it on Vita3K.\n3. If you want to install or boot Vitamin, it is not supported." + } +} diff --git a/i18n/lang/overlay_en.json b/i18n/lang/overlay_en.json new file mode 100644 index 000000000..ff214a112 --- /dev/null +++ b/i18n/lang/overlay_en.json @@ -0,0 +1,158 @@ +{ + "common.ok": { + "message": "OK", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Generic OK button." + }, + "common.cancel": { + "message": "Cancel", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Generic cancel button." + }, + "common.yes": { + "message": "Yes", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Generic yes button." + }, + "common.no": { + "message": "No", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Generic no button." + }, + "common.delete": { + "message": "Delete", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Generic delete action label." + }, + "common.submit": { + "message": "Submit", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Generic submit action label." + }, + "common.please_wait": { + "message": "Please wait...", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Progress text while waiting." + }, + "common.an_error_occurred": { + "message": "An error occurred.\nError code: {}", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Error body with one format placeholder for the error code." + }, + "common.could_not_load": { + "message": "Could not load the file.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. File load failure message." + }, + "common.could_not_save": { + "message": "Could not save the file.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. File save failure message." + }, + "common.file_corrupted": { + "message": "The file is corrupt.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Corrupt file message." + }, + "common.microphone_disabled": { + "message": "Enable the microphone.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Message shown when microphone access is unavailable." + }, + "dialog.save_data.save.title": { + "message": "Save", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Title for the save-data save list dialog." + }, + "dialog.save_data.load.title": { + "message": "Load", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Title for the save-data load list dialog." + }, + "dialog.save_data.save.saving": { + "message": "Saving...", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Progress message while saving save data." + }, + "dialog.save_data.load.loading": { + "message": "Loading...", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Progress message while loading save data." + }, + "dialog.save_data.save.saving_complete": { + "message": "Saving complete.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Completion message after save data finishes saving." + }, + "dialog.save_data.load.load_complete": { + "message": "Loading complete.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Completion message after save data finishes loading." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Deletion complete.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Completion message after save data finishes deleting." + }, + "dialog.save_data.save.new_saved_data": { + "message": "New Saved Data", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Label for a brand new save slot." + }, + "dialog.save_data.load.no_saved_data": { + "message": "There is no saved data.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Message shown when there is no saved data to load." + }, + "dialog.save_data.save.save_the_data": { + "message": "Do you want to save the data?", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Confirmation prompt before saving data." + }, + "dialog.save_data.load.load_saved_data": { + "message": "Do you want to load this saved data?", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Confirmation prompt before loading saved data." + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Do you want to delete this saved data?", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Confirmation prompt before deleting saved data." + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Do you want to overwrite this saved data?", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Confirmation prompt before overwriting saved data." + }, + "dialog.save_data.save.cancel_saving": { + "message": "Do you want to cancel saving?", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Confirmation prompt before cancelling a save operation." + }, + "dialog.save_data.load.cancel_loading": { + "message": "Do you want to cancel loading?", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Confirmation prompt before cancelling a load operation." + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Do you want to cancel deleting?", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Confirmation prompt before cancelling a delete operation." + }, + "dialog.save_data.save.warning_saving": { + "message": "Saving...\nDo not power off the system or close the application.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Progress warning while save data is being written." + }, + "dialog.save_data.save.could_not_save": { + "message": "Could not save the file.\nThere is not enough free space on the memory card. To save your progress in the application, you must create at least {} of free space.\n\nTo create the free space, press PS button to pause this application, and then delete other applications or content.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Save-data dialog error with one format placeholder for required free space." + }, + "dialog.save_data.save.not_free_space": { + "message": "There is not enough free space on the memory card. \nTo continue using the application, you must create at least {} of free space.\n\nPress the PS button to pause this application, and then delete other applications or content.", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Not-enough-free-space message with one format placeholder for required free space." + }, + "dialog.save_data.info.details": { + "message": "Details", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Save-data details label." + }, + "dialog.save_data.info.updated": { + "message": "Updated", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Save-data updated label." + }, + "dialog.trophy.preparing_start_app": { + "message": "Preparing to start the application...", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Trophy startup progress message shown before launching an app." + }, + "overlay.trophy_earned": { + "message": "You have earned a trophy!", + "description": "Firmware-derived string. Do Not Edit Unless Translation Missing. Trophy-earned renderer notification text." + }, + "message.load_app_failed": { + "message": "Failed to load \"{}\".\nCheck vita3k.log to see console output for details.\n1. Have you installed the firmware?\n2. Re-dump your own PS Vita app/game and install it on Vita3K.\n3. If you want to install or boot Vitamin, it is not supported.", + "description": "Detailed app-load failure message with one format placeholder for the path or title." + }, + "overlay.emulation_paused": { + "message": "Emulation Paused", + "description": "Pause overlay title." + }, + "overlay.press_ps_to_continue": { + "message": "Press PS BUTTON to continue", + "description": "Pause overlay subtitle prompting the user to resume emulation." + }, + "overlay.compiling_shaders": { + "message": "Please wait, compiling shaders...", + "description": "Overlay hint shown while shaders compile." + } +} diff --git a/i18n/lang/overlay_es.json b/i18n/lang/overlay_es.json new file mode 100644 index 000000000..4d59e25c7 --- /dev/null +++ b/i18n/lang/overlay_es.json @@ -0,0 +1,98 @@ +{ + "common.cancel": { + "message": "Cancelar" + }, + "common.yes": { + "message": "Sí" + }, + "common.delete": { + "message": "Eliminar" + }, + "common.please_wait": { + "message": "Espera un momento..." + }, + "common.an_error_occurred": { + "message": "Ha ocurrido un error.\nCódigo del error: {}" + }, + "common.could_not_load": { + "message": "No se ha podido cargar el archivo." + }, + "common.could_not_save": { + "message": "No se ha podido guardar el archivo." + }, + "common.file_corrupted": { + "message": "El archivo está dañado." + }, + "common.microphone_disabled": { + "message": "Activa el micrófono." + }, + "dialog.save_data.save.title": { + "message": "Guardar" + }, + "dialog.save_data.load.title": { + "message": "Cargar" + }, + "dialog.save_data.save.saving": { + "message": "Guardando..." + }, + "dialog.save_data.load.loading": { + "message": "Cargando..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Guardado." + }, + "dialog.save_data.load.load_complete": { + "message": "Carga completada." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Eliminación completada." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nuevos datos guardados" + }, + "dialog.save_data.load.no_saved_data": { + "message": "No hay datos guardados." + }, + "dialog.save_data.save.save_the_data": { + "message": "¿Quieres guardar los datos?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "¿Quieres cargar estos datos guardados?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "¿Quieres eliminar estos datos guardados?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "¿Quieres sobrescribir estos datos guardados?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "¿Quieres cancelar el guardado?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "¿Quieres cancelar la carga?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "¿Quieres cancelar la eliminación?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Guardando...\nNo apagues el sistema ni cierres la aplicación." + }, + "dialog.save_data.save.could_not_save": { + "message": "No se ha podido guardar el archivo.\nNo hay suficiente espacio libre en la tarjeta de memoria. Para guardar tu progreso en la aplicación, tienes que crear al menos {} de espacio libre.\n\nPara crear espacio libre, pulsa el botón PS para poner la aplicación en pausa y borrar otras aplicaciones o contenido." + }, + "dialog.save_data.save.not_free_space": { + "message": "No hay suficiente espacio libre en la tarjeta de memoria.\nPara seguir usando la aplicación, tienes que crear al menos {} de espacio libre.\n\nPulsa el botón PS para poner la aplicación en pausa y borrar otras aplicaciones o contenido." + }, + "dialog.save_data.info.details": { + "message": "Detalles" + }, + "dialog.save_data.info.updated": { + "message": "Actualizado" + }, + "dialog.trophy.preparing_start_app": { + "message": "Preparándose para iniciar la aplicación..." + }, + "overlay.trophy_earned": { + "message": "¡Has ganado un trofeo!" + } +} diff --git a/i18n/lang/overlay_fi.json b/i18n/lang/overlay_fi.json new file mode 100644 index 000000000..406e35944 --- /dev/null +++ b/i18n/lang/overlay_fi.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Peruuta" + }, + "common.yes": { + "message": "Kyllä" + }, + "common.no": { + "message": "Ei" + }, + "common.delete": { + "message": "Poista" + }, + "common.please_wait": { + "message": "Odota hetki..." + }, + "common.an_error_occurred": { + "message": "Virhe on tapahtunut.\nVirhekoodi: {}" + }, + "common.could_not_load": { + "message": "Tiedostoa ei voitu ladata." + }, + "common.could_not_save": { + "message": "Tiedostoa ei voitu tallentaa." + }, + "common.file_corrupted": { + "message": "Tiedosto on vioittunt." + }, + "common.microphone_disabled": { + "message": "Ota mikrofoni käyttöön." + }, + "dialog.save_data.save.title": { + "message": "Tallenna" + }, + "dialog.save_data.load.title": { + "message": "Lataa" + }, + "dialog.save_data.save.saving": { + "message": "Tallennetaan..." + }, + "dialog.save_data.load.loading": { + "message": "Ladataan..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Tallennus suoritettu." + }, + "dialog.save_data.load.load_complete": { + "message": "Lataus suoritettu." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Poistaminen suoritettu." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Uusi tallennettu tieto" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Tallennettua tietoa ei ole saatavilla." + }, + "dialog.save_data.save.save_the_data": { + "message": "Haluatko tallentaa tiedot?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Haluatko ladata tallennetun tiedon?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Haluatko poistaa nämä tallennetut tiedot?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Haluatko kirjoittaa tallennetun tiedon päälle?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Haluatko peruuttaa tallennuksen?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Haluatko peruuttaa latauksen?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Haluatko peruuttaa poistamisen?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Tallennetaan...\nÄlä katkaise virtaa järjestelmästä tai sulje sovellusta." + }, + "dialog.save_data.save.could_not_save": { + "message": "Tiedostoa ei voitu tallentaa.\nMuistikortilla ei ole tarpeeksi vapaata tilaa. Jotta voit tallentaa sovelluksen käytön, sinun on vapautettava vähintään {} tallennustilaa.\n\nVoit vapauttaa tallennustilaa painamalla PS-näppäintä, jolloin sovellus siirtyy tauolle, ja poista sitten muut sovellukset tai muu sisältö." + }, + "dialog.save_data.save.not_free_space": { + "message": "Muistikortilla ei ole tarpeeksi vapaata tilaa.\nJotta voit jatkaa sovelluksen käyttämistä, sinun on vapautettava vähintään {} tallennustilaa.\n\nAseta sovellus tauolle painamalla PS-näppäintä, ja poista sitten muut sovellukset tai muu sisältö." + }, + "dialog.save_data.info.details": { + "message": "Lisätiedot" + }, + "dialog.save_data.info.updated": { + "message": "Päivitetty" + }, + "dialog.trophy.preparing_start_app": { + "message": "Valmistellaan sovelluksen käynnistämistä..." + }, + "overlay.trophy_earned": { + "message": "Olet ansainnut trophyn!" + } +} diff --git a/i18n/lang/overlay_fr.json b/i18n/lang/overlay_fr.json new file mode 100644 index 000000000..396b2db16 --- /dev/null +++ b/i18n/lang/overlay_fr.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Annuler" + }, + "common.yes": { + "message": "Oui" + }, + "common.no": { + "message": "Non" + }, + "common.delete": { + "message": "Supprimer" + }, + "common.please_wait": { + "message": "Merci de patienter..." + }, + "common.an_error_occurred": { + "message": "Une erreur est survenue.\nCode d'erreur: {}" + }, + "common.could_not_load": { + "message": "Le chargement du fichier a échoué." + }, + "common.could_not_save": { + "message": "La sauvegarde du fichier a échoué." + }, + "common.file_corrupted": { + "message": "Le fichier est corrompu." + }, + "common.microphone_disabled": { + "message": "Activez le microphone." + }, + "dialog.save_data.save.title": { + "message": "Sauvegarder" + }, + "dialog.save_data.load.title": { + "message": "Charger" + }, + "dialog.save_data.save.saving": { + "message": "Sauvegarde en cours..." + }, + "dialog.save_data.load.loading": { + "message": "Chargement en cours..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Sauvegarde terminée." + }, + "dialog.save_data.load.load_complete": { + "message": "Chargement terminé." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Suppression terminée." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nouvelles données sauvegardées" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Aucune donnée sauvegardée." + }, + "dialog.save_data.save.save_the_data": { + "message": "Sauvegarder les données ?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Charger ces données sauvegardées ?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Supprimer ces données sauvegardées ?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Écraser ces données sauvegardées ?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Annuler la sauvegarde ?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Annuler le chargement ?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Annuler la suppression ?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Sauvegarde en cours...\nNe pas éteindre le système, ni fermer l'application." + }, + "dialog.save_data.save.could_not_save": { + "message": "La sauvegarde du fichier a échoué.\nEspace libre insuffisant sur la carte mémoire. Pour sauvegarder votre progression dans cette application, vous devez libérer au moins {} d'espace.\n\nPour libérer de l'espace, appuyez sur la touche PS pour mette cette application en pause, puis supprimez d'autres applications ou contenus." + }, + "dialog.save_data.save.not_free_space": { + "message": "Espace libre insuffisant sur la carte mémoire.\nPour continuer à utiliser cette application,vous devez libérer au moins {} d'espace.\n\nAppuyez sur la touche PS pour mette cette application en pause, puis supprimez d'autres applications ou contenus." + }, + "dialog.save_data.info.details": { + "message": "Détails" + }, + "dialog.save_data.info.updated": { + "message": "Mis à jour" + }, + "dialog.trophy.preparing_start_app": { + "message": "Préparation au lancement de l'application..." + }, + "overlay.trophy_earned": { + "message": "Vous avez obtenu un trophée !" + } +} diff --git a/i18n/lang/overlay_it.json b/i18n/lang/overlay_it.json new file mode 100644 index 000000000..8c9af98bb --- /dev/null +++ b/i18n/lang/overlay_it.json @@ -0,0 +1,98 @@ +{ + "common.cancel": { + "message": "Annulla" + }, + "common.yes": { + "message": "Sì" + }, + "common.delete": { + "message": "Elimina" + }, + "common.please_wait": { + "message": "Attendi..." + }, + "common.an_error_occurred": { + "message": "Si è verificato un errore.\nCodice di errore: {}" + }, + "common.could_not_load": { + "message": "Impossibile caricare il file." + }, + "common.could_not_save": { + "message": "Impossibile salvare il file." + }, + "common.file_corrupted": { + "message": "Il file è danneggiato." + }, + "common.microphone_disabled": { + "message": "Attiva il microfono." + }, + "dialog.save_data.save.title": { + "message": "Salva" + }, + "dialog.save_data.load.title": { + "message": "Carica" + }, + "dialog.save_data.save.saving": { + "message": "Salvataggio in corso..." + }, + "dialog.save_data.load.loading": { + "message": "Caricamento in corso..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Salvataggio completato." + }, + "dialog.save_data.load.load_complete": { + "message": "Caricamento completato." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Eliminazione completata." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nuovi dati salvati" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Non ci sono dati salvati." + }, + "dialog.save_data.save.save_the_data": { + "message": "Vuoi salvare i dati?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Vuoi caricare questi dati salvati?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Vuoi eliminare questi dati salvati?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Vuoi sovrascrivere questi dati salvati?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Vuoi annullare il salvataggio?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Vuoi annullare il caricamento?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Vuoi annullare l'eliminazione?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Salvataggio in corso...\nNon spegnere il sistema né chiudere l'applicazione." + }, + "dialog.save_data.save.could_not_save": { + "message": "Impossibile salvare il file.\nSpazio libero insufficiente sulla scheda di memoria. Per potere salvare l'avanzamento dell'applicazione, devono essere disponibili almeno {} di spazio libero.\n\nPer creare spazio libero, premi il tasto PS per mettere in pausa l'applicazione, quindi elimina altre applicazioni o del contenuto." + }, + "dialog.save_data.save.not_free_space": { + "message": "Spazio libero insufficiente sulla scheda di memoria.\nPer continuare a usare l'applicazione, devono essere disponibili almeno {} di spazio libero.\n\nPremi il tasto PS per mettere in pausa l'applicazione, quindi elimina altre applicazioni o del contenuto." + }, + "dialog.save_data.info.details": { + "message": "Dettagli" + }, + "dialog.save_data.info.updated": { + "message": "Aggiornato" + }, + "dialog.trophy.preparing_start_app": { + "message": "Preparazione per l'avvio dell'applicazione..." + }, + "overlay.trophy_earned": { + "message": "Hai guadagnato un trofeo!" + } +} diff --git a/i18n/lang/overlay_ja.json b/i18n/lang/overlay_ja.json new file mode 100644 index 000000000..340baa94d --- /dev/null +++ b/i18n/lang/overlay_ja.json @@ -0,0 +1,104 @@ +{ + "common.cancel": { + "message": "キャンセル" + }, + "common.yes": { + "message": "はい" + }, + "common.no": { + "message": "いいえ" + }, + "common.delete": { + "message": "削除" + }, + "common.submit": { + "message": "確認" + }, + "common.please_wait": { + "message": "しばらくお待ちください..." + }, + "common.an_error_occurred": { + "message": "エラーが起きました。\nエラーコード:{}" + }, + "common.could_not_load": { + "message": "ロードできませんでした。" + }, + "common.could_not_save": { + "message": "セーブできませんでした。" + }, + "common.file_corrupted": { + "message": "ファイルが壊れています。" + }, + "common.microphone_disabled": { + "message": "マイクを有効にしてください。" + }, + "dialog.save_data.save.title": { + "message": "セーブ" + }, + "dialog.save_data.load.title": { + "message": "ロード" + }, + "dialog.save_data.save.saving": { + "message": "セーブ中です..." + }, + "dialog.save_data.load.loading": { + "message": "ロード中です..." + }, + "dialog.save_data.save.saving_complete": { + "message": "セーブしました。" + }, + "dialog.save_data.load.load_complete": { + "message": "ロードしました。" + }, + "dialog.save_data.delete.deletion_complete": { + "message": "削除しました。" + }, + "dialog.save_data.save.new_saved_data": { + "message": "新しいセーブデータ" + }, + "dialog.save_data.load.no_saved_data": { + "message": "セーブデータがありません。" + }, + "dialog.save_data.save.save_the_data": { + "message": "データをセーブしますか?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "このセーブデータをロードしますか?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "このセーブデータを削除しますか?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "このセーブデータを上書きしますか?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "セーブをキャンセルしますか?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "ロードをキャンセルしますか?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "削除をキャンセルしますか?" + }, + "dialog.save_data.save.warning_saving": { + "message": "セーブ中です...\n電源を切ったり、アプリケーションを終了したりしないでください。" + }, + "dialog.save_data.save.could_not_save": { + "message": "セーブできませんでした。\nメモリーカードの空き容量が不足しています。アプリケーションの進み具合をセーブするには、{}以上の空き容量を用意する必要があります。\n\n空き容量を用意するには、PSボタンを押してこのアプリケーションを一時停止し、他のアプリケーションやコンテンツを削除してください。" + }, + "dialog.save_data.save.not_free_space": { + "message": "メモリーカードの空き容量が不足しています。\nアプリケーションを続けるには、{}以上の空き容量を用意する必要があります。\n\nPSボタンを押してこのアプリケーションを一時停止し、他のアプリケーションやコンテンツを削除してください。" + }, + "dialog.save_data.info.details": { + "message": "詳細" + }, + "dialog.save_data.info.updated": { + "message": "更新日" + }, + "dialog.trophy.preparing_start_app": { + "message": "アプリケーションを始める準備中です..." + }, + "overlay.trophy_earned": { + "message": "トロフィーを獲得しました!" + } +} diff --git a/i18n/lang/overlay_ko.json b/i18n/lang/overlay_ko.json new file mode 100644 index 000000000..fc957445f --- /dev/null +++ b/i18n/lang/overlay_ko.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "취소" + }, + "common.yes": { + "message": "예" + }, + "common.no": { + "message": "아니요" + }, + "common.delete": { + "message": "삭제" + }, + "common.please_wait": { + "message": "잠시 기다려 주십시오..." + }, + "common.an_error_occurred": { + "message": "에러기 발생했습니다.\n에러 코드: {}" + }, + "common.could_not_load": { + "message": "불러오지 못했습니다." + }, + "common.could_not_save": { + "message": "저장하지 못했습니다." + }, + "common.file_corrupted": { + "message": "파일이 손상되어 있습니다." + }, + "common.microphone_disabled": { + "message": "마이크를 활성화해 주십시오." + }, + "dialog.save_data.save.title": { + "message": "저장" + }, + "dialog.save_data.load.title": { + "message": "불러오기" + }, + "dialog.save_data.save.saving": { + "message": "저장중입니다..." + }, + "dialog.save_data.load.loading": { + "message": "불러오기 중입니다..." + }, + "dialog.save_data.save.saving_complete": { + "message": "저장했습니다." + }, + "dialog.save_data.load.load_complete": { + "message": "불러오기를 완료했습니다." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "삭제했습니다." + }, + "dialog.save_data.save.new_saved_data": { + "message": "새 저장 데이터" + }, + "dialog.save_data.load.no_saved_data": { + "message": "저장 데이터가 없습니다." + }, + "dialog.save_data.save.save_the_data": { + "message": "데이터를 저장하시겠습니까?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "이 저장 데이터를 불러오시겠습니까?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "이 저장 데이터를 삭제하시겠습니까?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "이 저장 데이터를 덮어쓰시겠습니까?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "저장을 취소하시겠습니까?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "불러오기를 취소하시겠습니까?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "삭제를 취소하시겠습니까?" + }, + "dialog.save_data.save.warning_saving": { + "message": "저장중입니다…\n전원을 끄거나 애플리케이션을 종료하지 마십시오." + }, + "dialog.save_data.save.could_not_save": { + "message": "저장하지 못했습니다.\n메모리 카드의 빈 용량이 부족합니다. 애플리케이션의 진행 상태를 저장하려면 {} 이상의 빈 용량이 필요합니다.\n\n빈 용량을 확보하시려면 PS 버튼을 눌러 이 애플리케이션을 일시 정지하신 후 다른 애플리케이션 및 콘텐츠를 삭제해 주십시오." + }, + "dialog.save_data.save.not_free_space": { + "message": "메모리 카드의 빈 용량이 부족합니다.\n애플리케이션을 계속하려면 {} 이상의 빈 용량이 필요합니다.\n\nPS 버튼을 눌러 이 애플리케이션을 일시 정지하신 후 다른 애플리케이션 및 콘텐츠를 삭제해 주십시오." + }, + "dialog.save_data.info.details": { + "message": "상세" + }, + "dialog.save_data.info.updated": { + "message": "갱신일" + }, + "dialog.trophy.preparing_start_app": { + "message": "애플리케이션을 시작하기 위한 준비중입니다..." + }, + "overlay.trophy_earned": { + "message": "홈 화면에 애플리케이션이 추가되었습니다." + } +} diff --git a/i18n/lang/overlay_nl.json b/i18n/lang/overlay_nl.json new file mode 100644 index 000000000..99433e40d --- /dev/null +++ b/i18n/lang/overlay_nl.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Annuleren" + }, + "common.yes": { + "message": "Ja" + }, + "common.no": { + "message": "Nee" + }, + "common.delete": { + "message": "Verwijderen" + }, + "common.please_wait": { + "message": "Een ogenblik geduld..." + }, + "common.an_error_occurred": { + "message": "Er is een fout opgetreden.\nFoutcode: {}" + }, + "common.could_not_load": { + "message": "Kan het bestand niet laden." + }, + "common.could_not_save": { + "message": "Kan het bestand niet opslaan." + }, + "common.file_corrupted": { + "message": "Het bestand is beschadigd." + }, + "common.microphone_disabled": { + "message": "Schakel de microfoon in." + }, + "dialog.save_data.save.title": { + "message": "Opslaan" + }, + "dialog.save_data.load.title": { + "message": "Laden" + }, + "dialog.save_data.save.saving": { + "message": "Bezig met opslaan..." + }, + "dialog.save_data.load.loading": { + "message": "Bezig met laden..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Opslaan voltooid." + }, + "dialog.save_data.load.load_complete": { + "message": "Het laden is voltooid." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Het verwijderen is voltooid." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nieuwe opgeslagen data" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Er zijn geen opgeslagen data." + }, + "dialog.save_data.save.save_the_data": { + "message": "Wil je de data opslaan?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Wil je deze opgeslagen data laden?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Wil je deze opgeslagen data verwijderen?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Wil je deze opgeslagen data overschrijven?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Wil je het opslaan annuleren?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Wil je het laden annuleren?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Wil je het verwijderen annuleren?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Bezig met opslaan...\nSchakel het systeem niet uit en sluit de applicatie niet." + }, + "dialog.save_data.save.could_not_save": { + "message": "Kan het bestand niet opslaan.\nEr is onvoldoende vrije ruimte op de geheugenkaart. Om je voortgang in de applicatie te kunnen opslaan, moet je tenminste {} vrije ruimte maken.\n\nOm vrije ruimte te maken, druk je op de PS-toets om deze applicatie te pauzeren en verwijder je vervolgens andere applicaties of content." + }, + "dialog.save_data.save.not_free_space": { + "message": "Er is onvoldoende vrije ruimte op de geheugenkaart.\nOm de applicatie te kunnen blijven gebruiken, moet je tenminste {} vrije ruimte maken.\n\nDruk op de PS-toets om deze applicatie te pauzeren en verwijder andere applicaties of content." + }, + "dialog.save_data.info.details": { + "message": "Details" + }, + "dialog.save_data.info.updated": { + "message": "Geüpdatet" + }, + "dialog.trophy.preparing_start_app": { + "message": "Bezig met het voorbereiden voor het starten van de applicatie..." + }, + "overlay.trophy_earned": { + "message": "Je hebt een trofee gewonnen!" + } +} diff --git a/i18n/lang/overlay_no.json b/i18n/lang/overlay_no.json new file mode 100644 index 000000000..881c911d0 --- /dev/null +++ b/i18n/lang/overlay_no.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Avbryt" + }, + "common.yes": { + "message": "Ja" + }, + "common.no": { + "message": "Nei" + }, + "common.delete": { + "message": "Slett" + }, + "common.please_wait": { + "message": "Vennligst vent..." + }, + "common.an_error_occurred": { + "message": "Det har oppstått en feil.\nFeilkode: {}" + }, + "common.could_not_load": { + "message": "Kunne ikke lagre filen." + }, + "common.could_not_save": { + "message": "Kunne ikke lagre filen." + }, + "common.file_corrupted": { + "message": "Filen er korrupt." + }, + "common.microphone_disabled": { + "message": "Aktiver mikrofonen." + }, + "dialog.save_data.save.title": { + "message": "Lagre" + }, + "dialog.save_data.load.title": { + "message": "Last" + }, + "dialog.save_data.save.saving": { + "message": "Lagrer..." + }, + "dialog.save_data.load.loading": { + "message": "Laster..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Lagring fullført." + }, + "dialog.save_data.load.load_complete": { + "message": "Lasting fullført." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Sletting fullført." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nylagret data" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Det finnes ingen lagret data." + }, + "dialog.save_data.save.save_the_data": { + "message": "Vil du lagre dataene?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Vil du laste disse lagrede dataene?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Vil du slette disse lagrede dataene?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Vil du overskrive disse lagrede dataene?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Vil du avbryte lagringen?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Vil du avbryte lastingen?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Vil du avbryte slettingen?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Lagrer...\nSlå ikke av systemet og lukk ikke programmet." + }, + "dialog.save_data.save.could_not_save": { + "message": "Kunne ikke lagre filen.\nDet er ikke nok ledig plass på minnekortet. For å lagre arbeidet ditt i programmet, må du frigjøre minimum {} ledig plass.\n\nFor å opprette denne ledige plassen må du trykke på PS-knappen for å stoppe dette programmet, og deretter slette andre programmer eller innhold." + }, + "dialog.save_data.save.not_free_space": { + "message": "Det er ikke nok ledig plass på minnekortet.\nFor å fortsette å bruke programmet, må du frigjøre minst {} ledig plass.\n\nTrykk på PS-knappen for å stoppe dette programmet og deretter slette andre programmer eller innhold." + }, + "dialog.save_data.info.details": { + "message": "Detaljer" + }, + "dialog.save_data.info.updated": { + "message": "Oppdatert" + }, + "dialog.trophy.preparing_start_app": { + "message": "Forbereder oppstart av programmet..." + }, + "overlay.trophy_earned": { + "message": "Du har oppnådd et trophy!" + } +} diff --git a/i18n/lang/overlay_pl.json b/i18n/lang/overlay_pl.json new file mode 100644 index 000000000..f98fe48f5 --- /dev/null +++ b/i18n/lang/overlay_pl.json @@ -0,0 +1,110 @@ +{ + "common.ok": { + "message": "OK" + }, + "common.cancel": { + "message": "Anuluj" + }, + "common.yes": { + "message": "Tak" + }, + "common.no": { + "message": "Nie" + }, + "common.delete": { + "message": "Usuń" + }, + "common.submit": { + "message": "Zatwierdź" + }, + "common.please_wait": { + "message": "Czekaj..." + }, + "common.an_error_occurred": { + "message": "Wystąpił błąd.\nKod błędu: {}" + }, + "common.could_not_load": { + "message": "Nie można załadować pliku." + }, + "common.could_not_save": { + "message": "Nie można zapisać pliku." + }, + "common.file_corrupted": { + "message": "Plik jest uszkodzony." + }, + "common.microphone_disabled": { + "message": "Włącz mikrofon." + }, + "dialog.save_data.save.title": { + "message": "Zapisz" + }, + "dialog.save_data.load.title": { + "message": "Załaduj" + }, + "dialog.save_data.save.saving": { + "message": "Zapisywanie..." + }, + "dialog.save_data.load.loading": { + "message": "Ładowanie..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Zapis ukończony." + }, + "dialog.save_data.load.load_complete": { + "message": "Ładowanie ukończone." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Usuwanie ukończone." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nowe zapisane dane" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Brak zapisanych danych." + }, + "dialog.save_data.save.save_the_data": { + "message": "Czy chcesz zapisać te dane?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Czy chcesz załadować zapisane dane?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Czy chcesz usunąć te zapisane dane?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Czy chcesz zastąpić zapisane dane?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Czy chcesz anulować zapis?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Czy chcesz anulować ładowanie?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Czy chcesz anulować usunięcie?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Zapisywanie...\nNie wyłączaj systemu ani nie zamykaj aplikacji." + }, + "dialog.save_data.save.could_not_save": { + "message": "Nie można zapisać pliku.\nNa karcie pamięci nie ma wystarczającej ilości wolnego miejsca. Na karcie pamięci nie ma wystarczającej ilości wolnego miejsca. Aby zapisać postęp w aplikacji, należy uzyskać co najmniej {} wolnego miejsca.\n\nAby uzyskać wolne miejsce, naciśnij przycisk PS w celu wstrzymania działania tej aplikacji, a następnie usuń inne aplikacje lub zawartość." + }, + "dialog.save_data.save.not_free_space": { + "message": "Na karcie pamięci nie ma wystarczającej ilości wolnego miejsca.\nAby kontynuować korzystanie z tej aplikacji, należy uzyskać co najmniej {} wolnego miejsca.\n\nNaciśnij przycisk PS, aby wstrzymać działanie tej aplikacji, a następnie usuń inne aplikacje lub zawartość." + }, + "dialog.save_data.info.details": { + "message": "Szczegóły" + }, + "dialog.save_data.info.updated": { + "message": "Zaktualizowano" + }, + "dialog.trophy.preparing_start_app": { + "message": "Przygotowanie do uruchomienia aplikacji..." + }, + "overlay.trophy_earned": { + "message": "Udało Ci się zdobyć trofeum!" + }, + "message.load_app_failed": { + "message": "Nie udało się wczytać \"{}\".\nSprawdź plik vita3k.log, aby zobaczyć szczegóły na konsoli.\n1. Czy zainstalowałeś oprogramowanie (firmware)?\n2. Zrób ponowny zrzut swojej aplikacji/gry PS Vita i zainstaluj ją na Vita3K.\n3. Instalacja lub uruchamianie Vitamin nie jest wspierane." + } +} diff --git a/i18n/lang/overlay_pt-BR.json b/i18n/lang/overlay_pt-BR.json new file mode 100644 index 000000000..9b9ed6c54 --- /dev/null +++ b/i18n/lang/overlay_pt-BR.json @@ -0,0 +1,104 @@ +{ + "common.cancel": { + "message": "Cancelar" + }, + "common.yes": { + "message": "Sim" + }, + "common.no": { + "message": "Não" + }, + "common.delete": { + "message": "Excluir" + }, + "common.submit": { + "message": "Enviar" + }, + "common.please_wait": { + "message": "Aguarde..." + }, + "common.an_error_occurred": { + "message": "Ocorreu um erro.\nCódigo de erro: {}" + }, + "common.could_not_load": { + "message": "Não foi possível carregar o arquivo." + }, + "common.could_not_save": { + "message": "Não foi possível salvar o arquivo." + }, + "common.file_corrupted": { + "message": "O arquivo está corrompido." + }, + "common.microphone_disabled": { + "message": "Ative o microfone." + }, + "dialog.save_data.save.title": { + "message": "Salvar" + }, + "dialog.save_data.load.title": { + "message": "Carregar" + }, + "dialog.save_data.save.saving": { + "message": "Salvando..." + }, + "dialog.save_data.load.loading": { + "message": "Carregando..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Gravação concluída." + }, + "dialog.save_data.load.load_complete": { + "message": "Carregamento concluído." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Exclusão concluída." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Novos dados salvos" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Não há dados salvos." + }, + "dialog.save_data.save.save_the_data": { + "message": "Deseja salvar os dados?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Deseja carregar esses dados salvos?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Deseja excluir esses dados salvos?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Deseja substituir esses dados salvos?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Deseja cancelar o salvamento?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Deseja cancelar o carregamento?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Deseja cancelar a exclusão?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Salvando...\nNão desligue o sistema nem feche o aplicativo." + }, + "dialog.save_data.save.could_not_save": { + "message": "Não foi possível salvar o arquivo.\nNão há espaço livre suficiente no cartão de memória. Para salvar o seu progresso no aplicativo, crie pelo menos {} de espaço livre.\n\nPara criar espaço livre, pressione o botão PS para pausar esse aplicativo e exclua outros aplicativos ou conteúdo." + }, + "dialog.save_data.save.not_free_space": { + "message": "Não há espaço livre suficiente no cartão de memória.\nPara continuar usando o aplicativo, crie pelo menos {} de espaço livre.\n\nPressione o botão PS para pausar o aplicativo e exclua outros aplicativos ou conteúdo." + }, + "dialog.save_data.info.details": { + "message": "Detalhes" + }, + "dialog.save_data.info.updated": { + "message": "Atualizado" + }, + "dialog.trophy.preparing_start_app": { + "message": "Preparando para iniciar o aplicativo..." + }, + "overlay.trophy_earned": { + "message": "Você conquistou um troféu!" + } +} diff --git a/i18n/lang/overlay_pt-PT.json b/i18n/lang/overlay_pt-PT.json new file mode 100644 index 000000000..8f7749baa --- /dev/null +++ b/i18n/lang/overlay_pt-PT.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Cancelar" + }, + "common.yes": { + "message": "Sim" + }, + "common.no": { + "message": "Não" + }, + "common.delete": { + "message": "Eliminar" + }, + "common.please_wait": { + "message": "Por favor aguarde..." + }, + "common.an_error_occurred": { + "message": "Ocorreu um erro.\nCódigo de erro: {}" + }, + "common.could_not_load": { + "message": "Não foi possível carregar o ficheiro." + }, + "common.could_not_save": { + "message": "Não foi possível guardar o ficheiro." + }, + "common.file_corrupted": { + "message": "O ficheiro está corrompido." + }, + "common.microphone_disabled": { + "message": "Ative o microfone." + }, + "dialog.save_data.save.title": { + "message": "Guardar" + }, + "dialog.save_data.load.title": { + "message": "Carregar" + }, + "dialog.save_data.save.saving": { + "message": "A guardar..." + }, + "dialog.save_data.load.loading": { + "message": "A carregar..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Guardado." + }, + "dialog.save_data.load.load_complete": { + "message": "Carregamento concluído." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Eliminação concluída." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Novos dados guardados" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Não existem dados guardados." + }, + "dialog.save_data.save.save_the_data": { + "message": "Pretende guardar os dados?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Pretende carregar estes dados guardados?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Pretende eliminar estes dados guardados?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Pretende gravar por cima destes dados guardados?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Pretende cancelar a operação de guardar?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Pretende cancelar o carregamento?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Pretende cancelar a eliminação?" + }, + "dialog.save_data.save.warning_saving": { + "message": "A guardar...\nNão desligue o sistema nem feche a aplicação." + }, + "dialog.save_data.save.could_not_save": { + "message": "Não foi possível guardar o ficheiro.\nNão existe espaço livre suficiente no cartão de memória. Não existe espaço livre suficiente no cartão de memória. Para guardar o progresso na aplicação, tem de criar, pelo menos, {} de espaço livre.\n\nPara criar o espaço livre, prima o botão PS para pausar esta aplicação e, em seguida, elimine outras aplicações ou conteúdo." + }, + "dialog.save_data.save.not_free_space": { + "message": "Não existe espaço livre suficiente no cartão de memória.\nPara continuar a utilizar a aplicação, tem de criar, pelo menos, {} de espaço livre.\n\nPrima o botão PS para pausar esta aplicação e, em seguida, elimine outras aplicações ou conteúdo." + }, + "dialog.save_data.info.details": { + "message": "Detalhes" + }, + "dialog.save_data.info.updated": { + "message": "Atualizado" + }, + "dialog.trophy.preparing_start_app": { + "message": "A preparar para iniciar a aplicação..." + }, + "overlay.trophy_earned": { + "message": "Ganhaste um troféu!" + } +} diff --git a/i18n/lang/overlay_ru.json b/i18n/lang/overlay_ru.json new file mode 100644 index 000000000..5aac02126 --- /dev/null +++ b/i18n/lang/overlay_ru.json @@ -0,0 +1,107 @@ +{ + "common.cancel": { + "message": "Отмена" + }, + "common.yes": { + "message": "Да" + }, + "common.no": { + "message": "Нет" + }, + "common.delete": { + "message": "Удалить" + }, + "common.submit": { + "message": "Подтвердить" + }, + "common.please_wait": { + "message": "Подождите..." + }, + "common.an_error_occurred": { + "message": "Произошла ошибка.\nКод ошибки: {}" + }, + "common.could_not_load": { + "message": "Не удалось загрузить файл." + }, + "common.could_not_save": { + "message": "Не удалось сохранить файл." + }, + "common.file_corrupted": { + "message": "Файл поврежден." + }, + "common.microphone_disabled": { + "message": "Включите микрофон." + }, + "dialog.save_data.save.title": { + "message": "Сохранить" + }, + "dialog.save_data.load.title": { + "message": "Загрузить" + }, + "dialog.save_data.save.saving": { + "message": "Идет сохранение..." + }, + "dialog.save_data.load.loading": { + "message": "Идет загрузка..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Сохранение завершено." + }, + "dialog.save_data.load.load_complete": { + "message": "Загрузка завершена." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Удаление завершено." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Новые сохраненные данные" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Отсутствуют сохраненные данные." + }, + "dialog.save_data.save.save_the_data": { + "message": "Сохранить данные?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Загрузить эти сохраненные данные?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Удалить эти сохраненные данные?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Перезаписать сохраненные данные?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Отменить сохранение?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Отменить загрузку?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Отменить удаление?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Идет сохранение...\nНе выключайте систему и не закрывайте приложение." + }, + "dialog.save_data.save.could_not_save": { + "message": "Не удалось сохранить файл.\nНа карте памяти недостаточно свободного места. Для сохранения процента выполнения приложения необходимо не менее {} свободного места.\n\nДля освобождения места нажмите кнопку PS, чтобы приостановить приложение, и удалите другие приложения или данные." + }, + "dialog.save_data.save.not_free_space": { + "message": "На карте памяти недостаточно свободного места.\nЧтобы продолжить использование приложения, необходимо не менее {} свободного места.\n\nНажмите кнопку PS, чтобы приостановить приложение, и удалите другие приложения или данные." + }, + "dialog.save_data.info.details": { + "message": "Подробности" + }, + "dialog.save_data.info.updated": { + "message": "Обновлен" + }, + "dialog.trophy.preparing_start_app": { + "message": "Подготовка к запуску приложения..." + }, + "overlay.trophy_earned": { + "message": "Вы получили приз!" + }, + "message.load_app_failed": { + "message": "Не удалось загрузить \"{}\".\nПроверьте vita3k.log, чтобы увидеть вывод консоли для получения подробной информации.\n1. Установлена ли у вас прошивка?\n2. Скопируйте собственное приложение(я)/игру(ы) и установите его на Vita 3K.\n3. Если вы хотите установить или запустить Vitamin, он не поддерживается." + } +} diff --git a/i18n/lang/overlay_sv.json b/i18n/lang/overlay_sv.json new file mode 100644 index 000000000..5f3925615 --- /dev/null +++ b/i18n/lang/overlay_sv.json @@ -0,0 +1,101 @@ +{ + "common.cancel": { + "message": "Avbryt" + }, + "common.yes": { + "message": "Ja" + }, + "common.no": { + "message": "Nej" + }, + "common.delete": { + "message": "Radera" + }, + "common.please_wait": { + "message": "Vänta..." + }, + "common.an_error_occurred": { + "message": "Ett fel har inträffat.\nFelkod: {}" + }, + "common.could_not_load": { + "message": "Kunde inte ladda filen." + }, + "common.could_not_save": { + "message": "Kunde inte spara filen." + }, + "common.file_corrupted": { + "message": "Filen är skadad." + }, + "common.microphone_disabled": { + "message": "Aktivera mikrofonen." + }, + "dialog.save_data.save.title": { + "message": "Spara" + }, + "dialog.save_data.load.title": { + "message": "Ladda" + }, + "dialog.save_data.save.saving": { + "message": "Sparar..." + }, + "dialog.save_data.load.loading": { + "message": "Laddar..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Sparning slutförd." + }, + "dialog.save_data.load.load_complete": { + "message": "Laddning slutförd." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Radering slutförd." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Nya sparade data" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Det finns inga sparade data." + }, + "dialog.save_data.save.save_the_data": { + "message": "Vill du spara datan?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Vill du ladda in dessa sparade data?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Vill du radera dessa sparade data?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Vill du skriva över dessa sparade data?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Vill du avbryta sparningen?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Vill du avbryta laddningen?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Vill du avbryta raderingen?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Sparar...\nStäng inte av systemet och stäng inte applikationen." + }, + "dialog.save_data.save.could_not_save": { + "message": "Kunde inte spara filen.\nDet finns inte tillräckligt med ledigt utrymme på minneskortet. Du måste skapa minst {} ledigt utrymme om du vill spara dina framsteg i den här applikationen.\n\nSkapa mer ledigt utrymme genom att trycka på PS-knappen för att pausa den här applikationen och radera sedan övriga applikationer eller data." + }, + "dialog.save_data.save.not_free_space": { + "message": "Det finns inte tillräckligt med ledigt utrymme på minneskortet.\nDu måste skapa minst {} ledigt utrymme om du vill fortsätta använda applikationen.\n\nTryck på PS-knappen för att pausa den här applikationen och radera sedan övriga applikationer eller innehåll." + }, + "dialog.save_data.info.details": { + "message": "Detaljer" + }, + "dialog.save_data.info.updated": { + "message": "Uppdaterat" + }, + "dialog.trophy.preparing_start_app": { + "message": "Förbereder start av applikationen..." + }, + "overlay.trophy_earned": { + "message": "Du har vunnit en trophy!" + } +} diff --git a/i18n/lang/overlay_tr.json b/i18n/lang/overlay_tr.json new file mode 100644 index 000000000..302f790b9 --- /dev/null +++ b/i18n/lang/overlay_tr.json @@ -0,0 +1,101 @@ +{ + "common.ok": { + "message": "Tamam" + }, + "common.cancel": { + "message": "İptal" + }, + "common.yes": { + "message": "Evet" + }, + "common.no": { + "message": "Hayır" + }, + "common.delete": { + "message": "Sil" + }, + "common.please_wait": { + "message": "Lütfen bekleyin..." + }, + "common.an_error_occurred": { + "message": "Bir hata oluştu.\nHata Kodu: {}" + }, + "common.could_not_load": { + "message": "Dosya yüklenemedi." + }, + "common.could_not_save": { + "message": "Dosya kaydedilemedi." + }, + "common.file_corrupted": { + "message": "Dosya bozuk." + }, + "dialog.save_data.save.title": { + "message": "Kaydet" + }, + "dialog.save_data.load.title": { + "message": "Yükle" + }, + "dialog.save_data.save.saving": { + "message": "Kaydediliyor..." + }, + "dialog.save_data.load.loading": { + "message": "Yükleniyor..." + }, + "dialog.save_data.save.saving_complete": { + "message": "Kaydetme tamamlandı." + }, + "dialog.save_data.load.load_complete": { + "message": "Yükleme tamamlandı." + }, + "dialog.save_data.delete.deletion_complete": { + "message": "Silme tamamlandı." + }, + "dialog.save_data.save.new_saved_data": { + "message": "Yeni Kaydedilmiş Veriler" + }, + "dialog.save_data.load.no_saved_data": { + "message": "Kayıtlı veri yok." + }, + "dialog.save_data.save.save_the_data": { + "message": "Verileri kaydetmek istiyor musunuz?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "Bu kayıtlı verileri yüklemek istiyor musunuz?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "Bu kayıtlı verileri silmek istiyor musunuz?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "Bu kayıtlı verilerin üzerine yazmak istiyor musunuz?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "Kaydetmeyi iptal etmek istiyor musunuz?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "Yüklemeyi iptal etmek istiyor musunuz?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "Silmeyi iptal etmek istiyor musunuz?" + }, + "dialog.save_data.save.warning_saving": { + "message": "Kaydediliyor...\nSistemi veya uygulamayı kapatmayın." + }, + "dialog.save_data.save.could_not_save": { + "message": "Dosya kaydedilemedi.\nHafıza kartında yeterli boş alan yok. Uygulamadaki ilerlemenizi kaydetmek için en az {} daha boş alan oluşturmanız gerekiyor.\n\nBoş alan oluşturmak için PS düğmesine basarak uygulamayı duraklatın ve ardından başka uygulamaları ya da içerikleri silin." + }, + "dialog.save_data.save.not_free_space": { + "message": "Hafıza kartında yeterli boş alan yok.\nUygulamayı kullanmaya devam etmek için en az {} daha boş alan oluşturmanız gerekiyor.\n\nUygulamayı duraklatmak için PS düğmesine basın ve ardından başka uygulamaları ya da içerikleri silin." + }, + "dialog.save_data.info.details": { + "message": "Ayrıntılı Bilgi" + }, + "dialog.save_data.info.updated": { + "message": "Güncelleme Tarihi" + }, + "dialog.trophy.preparing_start_app": { + "message": "Uygulama başlatma için hazırlanıyor..." + }, + "overlay.trophy_earned": { + "message": "Kupa kazandınız!" + } +} diff --git a/i18n/lang/overlay_zh-Hans.json b/i18n/lang/overlay_zh-Hans.json new file mode 100644 index 000000000..f37cebf9b --- /dev/null +++ b/i18n/lang/overlay_zh-Hans.json @@ -0,0 +1,107 @@ +{ + "common.cancel": { + "message": "取消" + }, + "common.yes": { + "message": "是" + }, + "common.no": { + "message": "否" + }, + "common.delete": { + "message": "删除" + }, + "common.submit": { + "message": "提交" + }, + "common.please_wait": { + "message": "请稍等…" + }, + "common.an_error_occurred": { + "message": "发生了错误。\n错误代码:{}" + }, + "common.could_not_load": { + "message": "无法读取文件。" + }, + "common.could_not_save": { + "message": "无法保存文件。" + }, + "common.file_corrupted": { + "message": "文件已损坏。" + }, + "common.microphone_disabled": { + "message": "请启用麦克风。" + }, + "dialog.save_data.save.title": { + "message": "保存" + }, + "dialog.save_data.load.title": { + "message": "读取" + }, + "dialog.save_data.save.saving": { + "message": "正在保存…" + }, + "dialog.save_data.load.loading": { + "message": "正在读取…" + }, + "dialog.save_data.save.saving_complete": { + "message": "保存完毕。" + }, + "dialog.save_data.load.load_complete": { + "message": "读取完毕。" + }, + "dialog.save_data.delete.deletion_complete": { + "message": "删除完毕。" + }, + "dialog.save_data.save.new_saved_data": { + "message": "新建保存数据" + }, + "dialog.save_data.load.no_saved_data": { + "message": "保存数据不存在。" + }, + "dialog.save_data.save.save_the_data": { + "message": "要保存数据吗?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "要读取此保存数据吗?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "要删除此保存数据吗?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "要覆盖此保存数据吗?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "要取消保存吗?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "要取消读取吗?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "要取消删除吗?" + }, + "dialog.save_data.save.warning_saving": { + "message": "正在保存…\n请勿关闭电源或关闭应用程序。" + }, + "dialog.save_data.save.could_not_save": { + "message": "无法保存文件。\n存储卡没有足够的空余容量。若要保存应用程序的进度,需准备{}以上的空余容量。\n\n若要准备空余容量,请按下PS键暂停此应用程序,并删除其它应用程序或内容。" + }, + "dialog.save_data.save.not_free_space": { + "message": "存储卡没有足够的空余容量。\n若要继续执行应用程序,请准备{}以上的空余容量。\n\n请按下PS键暂停此应用程序,并删除其它应用程序或内容。" + }, + "dialog.save_data.info.details": { + "message": "详细信息" + }, + "dialog.save_data.info.updated": { + "message": "升级日期" + }, + "dialog.trophy.preparing_start_app": { + "message": "正在准备启动应用程序…" + }, + "overlay.trophy_earned": { + "message": "已获得奖杯!" + }, + "message.load_app_failed": { + "message": "无法读取“{}”,\n请检查vita3k.log查看控制台输出的详细信息。\n1、您是否安装了固件?\n2、重新提取您已拥有的PS Vita应用程序/游戏,并在Vita3K中安装。\n3、如果您想要安装/运行Vitamin是不支持的。" + } +} diff --git a/i18n/lang/overlay_zh-Hant.json b/i18n/lang/overlay_zh-Hant.json new file mode 100644 index 000000000..1fb867203 --- /dev/null +++ b/i18n/lang/overlay_zh-Hant.json @@ -0,0 +1,107 @@ +{ + "common.cancel": { + "message": "取消" + }, + "common.yes": { + "message": "是" + }, + "common.no": { + "message": "否" + }, + "common.delete": { + "message": "刪除" + }, + "common.submit": { + "message": "提交" + }, + "common.please_wait": { + "message": "請稍候……" + }, + "common.an_error_occurred": { + "message": "發生錯誤。\n錯誤編號:{}" + }, + "common.could_not_load": { + "message": "無法載入。" + }, + "common.could_not_save": { + "message": "無法保存。" + }, + "common.file_corrupted": { + "message": "檔案已毀損。" + }, + "common.microphone_disabled": { + "message": "請啟用麥克風。" + }, + "dialog.save_data.save.title": { + "message": "保存" + }, + "dialog.save_data.load.title": { + "message": "載入" + }, + "dialog.save_data.save.saving": { + "message": "保存中……" + }, + "dialog.save_data.load.loading": { + "message": "載入中……" + }, + "dialog.save_data.save.saving_complete": { + "message": "已保存。" + }, + "dialog.save_data.load.load_complete": { + "message": "已載入。" + }, + "dialog.save_data.delete.deletion_complete": { + "message": "已刪除。" + }, + "dialog.save_data.save.new_saved_data": { + "message": "新的保存資料" + }, + "dialog.save_data.load.no_saved_data": { + "message": "找不到保存資料。" + }, + "dialog.save_data.save.save_the_data": { + "message": "確定要保存資料嗎?" + }, + "dialog.save_data.load.load_saved_data": { + "message": "確定要載入此保存資料嗎?" + }, + "dialog.save_data.delete.delete_saved_data": { + "message": "確定要刪除此保存資料嗎?" + }, + "dialog.save_data.save.overwrite_saved_data": { + "message": "確定要覆寫此保存資料嗎?" + }, + "dialog.save_data.save.cancel_saving": { + "message": "確定要取消保存嗎?" + }, + "dialog.save_data.load.cancel_loading": { + "message": "確定要取消載入嗎?" + }, + "dialog.save_data.delete.cancel_deleting": { + "message": "確定要取消刪除嗎?" + }, + "dialog.save_data.save.warning_saving": { + "message": "保存中……\n請勿關閉電源或結束應用程式。" + }, + "dialog.save_data.save.could_not_save": { + "message": "無法保存。\n記憶卡的可用容量不足。若要保存應用程式的進度,需準備{}以上的可用容量。\n\n若要準備可用容量,請按下PS按鈕暫停此應用程式,並刪除其他應用程式或內容。" + }, + "dialog.save_data.save.not_free_space": { + "message": "記憶卡的可用容量不足。\n若要繼續執行應用程式,請準備{}以上的可用容量。\n\n請按下PS按鈕暫停此應用程式,並刪除其他應用程式或內容。" + }, + "dialog.save_data.info.details": { + "message": "詳細內容" + }, + "dialog.save_data.info.updated": { + "message": "更新日" + }, + "dialog.trophy.preparing_start_app": { + "message": "正在準備啟動應用程式……" + }, + "overlay.trophy_earned": { + "message": "已獲得獎盃!" + }, + "message.load_app_failed": { + "message": "無法讀取“{}”,\n請檢查vita3k.log查閲控制臺輸出的詳細資訊。\n1、您是否安裝了韌體?\n2、重新傾印您已擁有的PS Vita應用程式/遊戲,並在Vita3K中安裝。\n3、如果您想要安裝/執行Vitamin是不支援的。" + } +} diff --git a/i18n/qt/vita3k_en.ts b/i18n/qt/vita3k_en.ts new file mode 100644 index 000000000..a9f0d729a --- /dev/null +++ b/i18n/qt/vita3k_en.ts @@ -0,0 +1,3704 @@ + + + + + AboutDialog + + + About Vita3K + + + + + Vita3K PlayStation Vita Emulator + + + + + Developers: + + + + + Contributors: + + + + + Supporters: + + + + + GitHub + + + + + Website + + + + + Ko-fi + + + + + Discord + + + + + Close + + + + + Version: %1 + + + + + Vita3K is the world's first functional PS Vita™/PS TV™ emulator, open-source and written in C++ for Windows, Linux, macOS, and Android. +Visit our website at <a href="https://vita3k.org/quickstart.html">vita3k.org</a> for more info. If you're interested in contributing, check out our <a href="https://github.com/Vita3K/Vita3K">GitHub</a>. If you want to support us, you can donate via <a href="https://ko-fi.com/vita3k">Ko-fi</a>. + + + + + Icon by %1 + + + + + AppsList + + + App Library + + + + + AppsListContextMenu + + + Delete Selected Applications + + + + + Delete Applications + + + + + Are you sure you want to delete %1 selected applications? + +This action cannot be undone. + + + + + Delete Shader Caches + + + + + Boot + + + + + View Live Area + + + + + + Compatibility + + + + + Check Compatibility + + + + + Open State Report + + + + + Copy Vita3K Summary + + + + + Create State Report + + + + + Update Database + + + + + Copy Info + + + + + Name and Serial + + + + + + Name + + + + + + Serial + + + + + App Summary + + + + + Custom Config + + + + + Create + + + + + Edit + + + + + Remove + + + + + Open Folder + + + + + + Application + + + + + + Save Data + + + + + + Patch + + + + + + DLC + + + + + + License + + + + + + Shader Cache + + + + + + Shader Log + + + + + + Export Textures + + + + + + Import Textures + + + + + Delete + + + + + Delete Application + + + + + Are you sure you want to delete %1 [%2]? + +This action cannot be undone. + + + + + Delete Save Data + + + + + Are you sure you want to delete the save data? + + + + + Delete Patch + + + + + Are you sure you want to delete the patch data? + + + + + Delete DLC + + + + + Are you sure you want to delete the DLC data? + + + + + Delete License + + + + + Are you sure you want to delete the license? + + + + + Other + + + + + Decrypt All SELF + + + + + Reset Last Time Played + + + + + + Update History + + + + + Version %1 + + + + + + Close + + + + + Information + + + + + App Information + + + + + Version + + + + + Category + + + + + Content ID + + + + + Parental Level + + + + + Size + + + + + Path + + + + + AppsListTable + + + Never + + + + + Playable + + + + + Ingame+ + + + + + Ingame + + + + + Menu + + + + + Intro + + + + + Boots + + + + + Nothing + + + + + Unknown + + + + + ArchiveInstallDialog + + + Install Archive + + + + + Select Install Type + + + + + What would you like to install? + + + + + Select a file (.zip / .vpk)... + + + + + Select a directory (installs all archives inside)... + + + + + Cancel + + + + + Select Archive + + + + + PlayStation Vita commercial software package (NoNpDrm/FAGDec) / PlayStation Vita homebrew software package (*zip, *.vpk);;PlayStation Vita commercial software package (NoNpDrm/FAGDec) (*.zip);;PlayStation Vita homebrew software package (*.vpk) + + + + + Select Directory Containing Archives + + + + + No Archives Found + + + + + The selected directory contains no .zip or .vpk files. + + + + + Installing... + + + + + Archive: 0 / %1 + + + + + Content: 0 / 0 + + + + + Already Installed + + + + + This content is already installed. +Do you want to overwrite it? + + + + + Delete archive files after install? + + + + + ControlsDialog + + + Controls & Controllers + + + + + Keyboard + + + + + + D-Pad + + + + + + + Up + + + + + + + Left + + + + + + + Right + + + + + + + Down + + + + + + Left Stick + + + + + GUI Shortcuts + + + + + Full Screen + + + + + Toggle Touch + + + + + + L + + + + + + R + + + + + + PS Button + + + + + + Select + + + + + + Start + + + + + + PS TV Mode + + + + + + L2 + + + + + + L3 + + + + + + R3 + + + + + + R2 + + + + + Miscellaneous + + + + + Toggle Texture Replacement + + + + + Take Screenshot + + + + + Pinch Modifier + + + + + Alternate Pinch In + + + + + Alternate Pinch Out + + + + + + Face Buttons + + + + + Triangle + + + + + Square + + + + + Circle + + + + + Cross + + + + + + Right Stick + + + + + Reset to Defaults + + + + + + Close + + + + + Controller 1 + + + + + Controller 2 + + + + + Controller 3 + + + + + Controller 4 + + + + + Unset + + + + + Button %1 + + + + + Axis %1 + + + + + Controls + + + + + [ Press a key… ] + + + + + Duplicate Key + + + + + The key "%1" is already assigned to another action. + + + + + + No controller connected. + + + + + Analog Stick Multiplier + + + + + Disable Motion Controls + + + + + Reset Controller Bindings + + + + + Controller: %1 + + + + + [ Press a button… ] + + + + + [ Move an axis… ] + + + + + DebugWidget + + + Debug + + + + + + + + + + + ID + + + + + + + + + + + + Name + + + + + Status + + + + + + Stack + + + + + Threads + + + + + + Lock Count + + + + + + + + + Attributes + + + + + + + + + + Waiting Threads + + + + + + Owner + + + + + Mutexes + + + + + LW Mutexes + + + + + Condvars + + + + + LW Condvars + + + + + + + Value + + + + + Max + + + + + Semaphores + + + + + Flags + + + + + Event Flags + + + + + Page + + + + + Address Range + + + + + Size (KiB) + + + + + Pages + + + + + Allocations + + + + + + Address + + + + + Count + + + + + Arch + + + + + Disassembly + + + + + + not owned + + + + + Thread Not Found + + + + + Thread 0x%1 no longer exists. + + + + + Thread: %1 (0x%2) + + + + + Name: + + + + + Status: + + + + + PC: + + + + + SP: + + + + + LR: + + + + + Executing: + + + + + Registers + + + + + Register + + + + + Offset + + + + + (invalid) + + + + + Nothing to disassemble. + + + + + Invalid address. + + + + + Disassembled %1 instructions. + + + + + FirmwareInstallDialog + + + Install Firmware + + + + + Select Firmware Package + + + + + PlayStation Vita Firmware Package (*.PUP *.pup) + + + + + Installing firmware, please wait… + + + + + Installation Failed + + + + + Failed to install firmware. +Check the log for details. + + + + + Firmware installed successfully!%1 + + + + + + +Version: %1 + + + + + Delete firmware file after install? + + + + + GameCompatibility + + + Failed to parse version response. + + + + + Failed to install compatibility database + + + + + LicenseInstallDialog + + + Install License + + + + + How would you like to install the license? + + + + + Select .bin / .rif file… + + + + + Enter zRIF key manually… + + + + + Cancel + + + + + Select License File + + + + + PlayStation Vita software license file (*.bin *.rif) + + + + + + Installation Failed + + + + + Failed to install the license file. +The file may be corrupted. + + + + + + License Installed + + + + + + License installed successfully! + +Content ID: %1 +Title ID: %2 + + + + + Enter zRIF Key + + + + + Paste your zRIF key: + + + + + Failed to create license from zRIF key. +The key may be invalid. + + + + + LiveAreaWidget + + + Start + + + + + Manual + + + + + %1 / %2 + + + + + LogWidget + + + Log + + + + + MainWindow + + + File + + + + + Emulation + + + + + + Settings + + + + + + Debug + + + + + Manage + + + + + View + + + + + Help + + + + + Tool Bar + + + + + Open + + + + + Refresh + + + + + + Stop + + + + + + + Play + + + + + + + Fullscreen + + + + + + Controls + + + + + Check for updates + + + + + About Vita3k + + + + + About Qt + + + + + Install Firmware (.PUP) + + + + + Install Package (.pkg) + + + + + Install Archive (.zip / .vpk) + + + + + Install License (.rif / work.bin) + + + + + + + Pause + + + + + Trophy Collection + + + + + User Management + + + + + Core + + + + + CPU + + + + + GPU + + + + + Audio + + + + + Camera + + + + + System + + + + + Emulator + + + + + GUI + + + + + Network + + + + + Show Tool Bar + + + + + Show Title Bars + + + + + Show Apps List + + + + + Show Log + + + + + View Welcome Dialog + + + + + Threads + + + + + Mutexes + + + + + Lightweight Mutexes + + + + + Condition Variables + + + + + Lightweight Condition Variables + + + + + Semaphores + + + + + Event Flags + + + + + Memory Allocations + + + + + Disassembly + + + + + + Search... + + + + + + Exit Fullscreen + + + + + Exit? + + + + + A game is still running. Do you really want to exit? + +Any unsaved progress will be lost! + + + + + Installing firmware… + + + + + Install Firmware + + + + + + Install Failed + + + + + Failed to install the dropped firmware file. +Check the log for details. + + + + + Already Installed + + + + + This content is already installed. +Do you want to overwrite it? + + + + + Failed to install the dropped license file. +The file may be corrupted. + + + + + Switch App? + + + + + An app is already running. +Do you want to close it and launch another app? + +Any unsaved progress will be lost! + + + + + + + Error + + + + + + Could not find app '%1' in apps list. + + + + + + %1 | %2 (%3) | Loading... + + + + + Could not create OpenGL context. +Does your GPU support at least OpenGL 4.4? + + + + + Could not make OpenGL context current. + + + + + Failed to initialise the renderer. + + + + + %1 | %2 (%3) | Initializing... + + + + + Failed to initialize emulator state. + + + + + %1 | %2 (%3) | Loading modules... + + + + + Failed to load app modules. + + + + + Failed to start game threads. + + + + + %1 | %2 (%3) | Please wait, loading... + + + + + Pre-install package + + + + + Main firmware + + + + + Font package + + + + + Missing Firmware + + + + + Firmware is not fully installed. + + + + + The following firmware components are missing: +- %1 + +Games may fail to boot or render correctly until they are installed. + + + + + Don't show this warning again + + + + + Launch Anyway + + + + + %1 | %2 (%3) + + + + + Exit Game? + + + + + Do you really want to exit the game? + +Any unsaved progress will be lost! + + + + + + Resume + + + + + Open Emulated Storage Path + + + + + Open Patch Path + + + + + Open Textures Path + + + + + Standard + + + + + High + + + + + + NGS: ON + + + + + + NGS: OFF + + + + + Mute + + + + + Reset to 100% + + + + + HIGH + + + + + STANDARD + + + + + VOLUME: MUTED + + + + + VOLUME: %1% + + + + + PkgInstallDialog + + + Install Package + + + + + Select Package File + + + + + PlayStation Store Downloaded Package (*.pkg) + + + + + Select License Type + + + + + No license was found automatically. +How would you like to provide it? + + + + + Select .bin / .rif file… + + + + + Enter zRIF key manually… + + + + + Cancel + + + + + Select License File + + + + + PlayStation Vita software license file (*.bin *.rif) + + + + + Enter zRIF Key + + + + + Paste your zRIF key: + + + + + Invalid zRIF Key + + + + + The zRIF key you entered is not valid. +Please check and try again. + + + + + Installing package, please wait… + + + + + Installation Failed + + + + + Failed to install the package. +Check the log for details. + + + + + Installation complete! + +%1 [%2] + + + + + Delete package file after install + + + + + Delete .bin / .rif file after install + + + + + QObject + + + Nothing + + + + + Bootable + + + + + Intro + + + + + Menu + + + + + In-Game (less) + + + + + In-Game (more) + + + + + Playable + + + + + Unknown + + + + + Never played + + + + + (incompatible or no content found) + + + + + Archive: %1 / %2 + + + + + Content: %1 / %2 + + + + + Up + + + + + Left + + + + + Right + + + + + Down + + + + + Platinum + + + + + Gold + + + + + Silver + + + + + Bronze + + + + + + Download Firmware %1 + + + + + Title + + + + + Title ID + + + + + Version + + + + + Category + + + + + Compatibility + + + + + Last Played + + + + + Time Played + + + + + SettingsDialog + + + Settings - %1 + + + + + Standard + + + + + High + + + + + + Current volume: %1% + + + + + Trace + + + + + Debug + + + + + Info + + + + + Warning + + + + + Error + + + + + Critical + + + + + Off + + + + + Minimum + + + + + Low + + + + + Medium + + + + + Maximum + + + + + Top Left + + + + + Top Center + + + + + Top Right + + + + + Bottom Left + + + + + Bottom Center + + + + + Bottom Right + + + + + + + Current emulator path: %1 + + + + + None + + + + + Japanese + + + + + English (US) + + + + + French + + + + + Spanish + + + + + German + + + + + Italian + + + + + Dutch + + + + + Portuguese (PT) + + + + + Russian + + + + + Korean + + + + + Chinese (Traditional) + + + + + Chinese (Simplified) + + + + + Finnish + + + + + Swedish + + + + + Danish + + + + + Norwegian + + + + + Polish + + + + + Portuguese (BR) + + + + + English (GB) + + + + + Turkish + + + + + YYYY/MM/DD + + + + + DD/MM/YYYY + + + + + MM/DD/YYYY + + + + + 12-Hour + + + + + 24-Hour + + + + + System Default + + + + + + Solid Color + + + + + + Static Image + + + + + + No image selected + + + + + File Loading Delay: %1 ms + + + + + Front Camera Color + + + + + Back Camera Color + + + + + Select Front Camera Image + + + + + + Images (*.png *.jpg *.jpeg *.bmp) + + + + + Select Back Camera Image + + + + + Select Emulator Storage Folder + + + + + + Unwatch Code + + + + + + Watch Code + + + + + + Unwatch Memory + + + + + + Watch Memory + + + + + + Unwatch Import Calls + + + + + + Watch Import Calls + + + + + Unsaved Changes + + + + + You have unsaved changes. What would you like to do? + + + + + None + Stylesheets + + + + + Native (%1) + Stylesheets + + + + + Default (Bright) + Stylesheets + + + + + Default (Dark) + Stylesheets + + + + + TrophyCollectionDialog + + + Trophy Collection + + + + + + Icon + + + + + App + + + + + Progress + + + + + Trophies + + + + + + Name + + + + + + Description + + + + + Grade + + + + + Status + + + + + ID + + + + + + + Earned + + + + + Progress: 0% (0/0) + + + + + + Not Earned + + + + + Hidden + + + + + Bronze + + + + + Silver + + + + + Gold + + + + + Platinum + + + + + Icon size: + + + + + ← Back to Apps + + + + + Loading trophies… + + + + + Cancel + + + + + %1% (%2/%3) + + + + + Progress: %1% (%2/%3) + + + + + — + + + + + Total: %1% (%2/%3 trophies across %4 apps) + + + + + &Copy Info + + + + + Name + Description + + + + + &Lock Trophy + + + + + &Unlock Trophy + + + + + Not permitted + + + + + Platinum trophies can only be unlocked in-game. + + + + + &Remove + + + + + Confirm delete + + + + + Delete all trophies for: +%1? + + + + + Unknown + + + + + UserManagementDialog + + + User Management + + + + + + Create User + + + + + Select User + + + + + + Delete User + + + + + Enter user name: + + + + + Are you sure you want to delete user '%1'? + +All user data will be lost! + + + + + WelcomeDialog + + + Vita3K is an open-source PlayStation Vita emulator written in C++ for Windows, Linux, macOS and Android. +The emulator is still in its development stages so any feedback and testing is greatly appreciated. + + + + + <div align="center">To get started, please install all PS Vita firmware files.<br><br>A comprehensive guide on how to set up Vita3K can be found on the <a href="https://vita3k.org/quickstart.html">Quickstart</a> page.<br>Consult the Commercial game and Homebrew compatibility lists to see what currently runs.<br><br>Contributions are welcome! <a href="https://github.com/Vita3K/Vita3K">GitHub</a><br>Additional support can be found in the #help channel on <a href="https://discord.gg/6aGwQzh">Discord</a>.</div> + + + + + Vita3K does not condone piracy. You must dump your own games. + + + + + Download Pre-Install Firmware + + + + + Download Firmware Font Package + + + + + Download Firmware + + + + + desc + + + Automatically select modules to load. Recommended for most apps. + + + + + Select this mode to load Automatic module and selected modules from the list below. + + + + + Only load the modules selected from the list. Advanced users only. + + + + + Enable Dynarmic CPU optimizations. +Disabling may improve compatibility at the cost of performance. + + + + + Select the backend renderer. +Vulkan is recommended for most systems. + + + + + Set the renderer accuracy level. High accuracy may improve visuals but reduce performance. + + + + + Enable V-Sync for OpenGL. Reduces tearing at the cost of potential input lag. + + + + + Disable surface synchronization. +May improve performance but can cause visual glitches. + + + + + Allow pipelines to be compiled concurrently on multiple concurrent threads. +This decreases pipeline compilation stutter at the cost of temporary graphical glitches. + + + + + Select the screen filter used to display the rendered image. + + + + + Enable upscaling for Vita3K. +Experimental: apps are not guaranteed to render properly at more than 1x. + + + + + Set the anisotropic filtering level. +Improves texture quality at oblique viewing angles. + + + + + Export textures used by the app to the textures folder. + + + + + Import replacement textures from the textures folder. + + + + + Enable shader caching. Reduces stuttering on subsequent runs. + + + + + Use SPIR-V shaders with Vulkan renderer. + + + + + Enable FPS hack. May unlock or double the framerate in some games, +but can cause speed issues in others. + + + + + Select the Vita memory mapping method used by the Vulkan renderer. +Higher methods improve accuracy but require more GPU features. + + + + + Select the audio backend. +SDL is the default; Cubeb offers lower latency on some systems. + + + + + Set the in-game audio volume. + + + + + Enable NGS (Next Generation Sound) support. +Required by many apps for proper audio. + + + + + Select the front camera source. +Solid Color or Static Image can be used as substitutes. + + + + + Select the back camera source. +Solid Color or Static Image can be used as substitutes. + + + + + Select which button acts as the Enter/Confirm button. + + + + + Enable PlayStation TV mode. +Some apps may behave differently or become playable. + + + + + Enable Show Mode. Cycles through the home screen automatically. + + + + + Enable Demo Mode. + + + + + Automatically enter fullscreen when booting an app. + + + + + Show the PS Vita Live Area screen before booting an app. + + + + + Show a hint when shaders are being compiled during gameplay. + + + + + Check for Vita3K updates when the emulator starts. + + + + + Log compatibility-related warnings for debugging. + + + + + Enable the texture cache. Improves performance in most apps. + + + + + Keep a duplicate of the log file named after the current title when an app runs. + + + + + Enable Discord Rich Presence to show current app status. + + + + + Show a performance overlay with FPS and other statistics. + + + + + Add an artificial delay to file loading. May fix timing issues in some apps. + + + + + Stretch the display area to fill the entire window, ignoring the original aspect ratio. + + + + + Use pixel-perfect integer scaling in fullscreen mode when the resolution is a multiple of the Vita native resolution. + + + + + Pretend to be signed in to PlayStation Network. +Required by some apps to function. + + + + + Enable HTTP networking support for apps that use it. + + + + + Select which local network address Vita3K should use for ad-hoc networking. + + + + + Log module import symbols. + + + + + Log module export symbols. + + + + + Log active shader programs. + + + + + Log shader uniform values. + + + + + Save color surfaces to files. + + + + + Enable Vulkan validation layers. +Useful for debugging but reduces performance. + + + + + Dump loaded code as ELFs. + + + + + Set the system language reported to applications. + + + + + Set the date format used by the emulated system. + + + + + Set the time format used by the emulated system. + + + + + Select which IME keyboard languages are available to applications. + + + + + Show the Welcome Dialog at startup. + + + + + Show a warning before launching a game when one or more firmware packages are missing. + + + + + Set the maximum number of log lines to keep in memory. +Set to 0 for infinite (no limit). + + + + + Select the visual theme for the application. +You can also place custom .qss files in the gui-configs folder. + + + + + Select the desktop interface language used by the Qt frontend. +Use System Default to follow the operating system language. + + + + + vita3k_settings_dialog + + + Settings + + + + + Core + + + + + Module Loading Mode + + + + + Automatic + + + + + Auto && Manual + + + + + Manual + + + + + LLE Modules List + + + + + Search modules... + + + + + Clear List + + + + + Refresh List + + + + + + + + + + + + + + Description + + + + + + + + + + + + + + Point your mouse at an option to display a description here. + + + + + CPU + + + + + Dynarmic Settings + + + + + Enable CPU Optimizations + + + + + + GPU + + + + + Backend Renderer + + + + + Graphics Device + + + + + Rendering Accuracy + + + + + Screen Filter + + + + + OpenGL Options + + + + + V-Sync + + + + + Surface Sync + + + + + Disable Surface Sync + + + + + Vulkan Options + + + + + Asynchronous Pipeline Compilation + + + + + Memory Mapping + + + + + Internal Resolution Upscaling + + + + + 0.5x + + + + + 8x + + + + + 960x544 + + + + + + Reset + + + + + Anisotropic Filtering + + + + + + 1x + + + + + 16x + + + + + Textures + + + + + Export Textures + + + + + Import Textures + + + + + Texture Exporting Format + + + + + Shaders + + + + + Enable Shader Cache + + + + + Use SPIR-V Shader (Vulkan) + + + + + Clean Shaders Cache and Log + + + + + Hacks + + + + + FPS Hack + + + + + Audio + + + + + Audio Backend + + + + + Volume + + + + + Current volume: 100% + + + + + Audio Settings + + + + + Enable NGS Support + + + + + Camera + + + + + Front Camera + + + + + + Camera Source + + + + + + Camera Color + + + + + + Choose Color... + + + + + + Camera Image + + + + + + No image selected + + + + + + Set Image... + + + + + Back Camera + + + + + System + + + + + Enter Button Assignment + + + + + Circle + + + + + Cross + + + + + System Modes + + + + + PlayStation TV Mode (PSTV) + + + + + Show Mode + + + + + Demo Mode + + + + + System Settings + + + + + System Language + + + + + Date Format + + + + + Time Format + + + + + IME Languages + + + + + Emulator + + + + + General + + + + + Boot Games in Fullscreen + + + + + Show Live Area Before Booting + + + + + Show Shader Compilation Hint + + + + + Check for Updates + + + + + Log Compatibility Warnings + + + + + Enable Texture Cache + + + + + Archive Log + + + + + Enable Discord Rich Presence + + + + + Log Level + + + + + + Performance Overlay + + + + + Overlay Detail + + + + + Overlay Position + + + + + Display + + + + + Stretch the Display Area + + + + + Fullscreen HD Pixel Perfect + + + + + Emulated System Storage Folder + + + + + Current emulator path: (not set) + + + + + Change Emulator Path + + + + + Reset Emulator Path + + + + + Custom Config Settings + + + + + Clear All Custom Configs + + + + + Screenshot + + + + + Screenshot Format: + + + + + File Loading + + + + + File Loading Delay: 0 ms + + + + + GUI + + + + + Log + + + + + Log Buffer Size: + + + + + Infinite + + + + + UI Options + + + + + Interface Language + + + + + Show Welcome Screen + + + + + Warn When Firmware Is Missing + + + + + Stylesheet + + + + + Apply Theme + + + + + Network + + + + + PlayStation Network + + + + + PSN Signed In + + + + + Ad-Hoc + + + + + Ad-Hoc Address + + + + + Subnet Mask: + + + + + HTTP + + + + + Enable HTTP Networking + + + + + Timeout Attempts + + + + + Timeout Sleep + + + + + Read End Attempts + + + + + Read End Sleep + + + + + Debug + + + + + Logging + + + + + Import Logging + + + + + Export Logging + + + + + Log Active Shaders + + + + + Log Shader Uniforms + + + + + Save color surfaces + + + + + Vulkan Validation Layer + + + + + Watch Controls + + + + + Watch Code + + + + + Watch Memory + + + + + Watch Import Calls + + + + + Miscellaneous + + + + + ELF Dumping + + + + + welcome_dialog + + + Welcome to Vita3K + + + + + Vita3K PlayStation Vita Emulator + + + + + Commercial Compatibility List + + + + + Homebrew Compatibility List + + + + + Ok + + + + + Show next time + + + + diff --git a/lang/system/da.xml b/lang/system/da.xml deleted file mode 100644 index 223c88f2c..000000000 --- a/lang/system/da.xml +++ /dev/null @@ -1,397 +0,0 @@ - - - - - - Open Pref Path - Install Firmware - Install .pkg - Install .zip, .vpk - Install License - Afslut - - - Last Apps used - - - Brugeradministration - - - Keyboard Controls - - - Welcome - - - - - - Vejledning - Opdater - - - Dette program og alle relaterede data, herunder gemte data, vil blive slettet. - - Opdateringshistorik - Version {} - - Kvalificeret - Ikke kvalificeret - Niveau {} - Navn - Trophy opnåelse - Forældrekontrol - Opdateret - Størrelse - Version - - - - - Der er opstået en fejl. -Fejlkode: {} - Afbryd - Lukke - Kunne ikke indlæse filen. - Kunne ikke gemme filen. - Slet - Fejl - Filen er defekt. - Aktivér mikrofonen. - Nej - Vent venligst... - Vælg alle - - Ja - - søndag - mandag - tirsdag - onsdag - torsdag - fredag - lørdag - - - Januar - Februar - Marts - April - Maj - Juni - Juli - August - September - Oktober - November - December - - - januar - februar - marts - april - maj - juni - juli - august - september - oktober - november - december - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Skjult Trophy - 1 time siden - 1 minut siden - Bronze - Gold - Platinum - Silver - {} timer siden - {} minutter siden - - - - - De valgte programmer og alle relaterede data, herunder gemte data, vil blive slettet. - Der er ingen indholdsenheder. - - - De valgte gemte dataelementer vil blive slettede. - Der er ingen gemte data. - - Tema - Ledig plads - Ryd alle - - - - Num - - - - - Forbereder start af programmet... - - - - Ønsker du at afbryde sletningen? - Sletning udført. - Ønsker du at slette disse gemte data? - - -
Detaljer
- Opdateret -
- - Ønsker du at afbryde indlæsningen? - Der er ingen gemte data. - Indlæsning udført. - Indlæser... - Ønsker du at indlæse disse gemte data? - - - Ønsker du at afbryde lagringen? - Kunne ikke gemme filen. -Der er ikke nok ledig plads på hukommelseskortet. Du skal skabe mindst {} ledig plads for at gemme dit forløb i programmet. - -Tryk på PS-knappen for at sætte programmet på pause, og slet derefter de andre programmer eller indhold for at skabe den ledige plads. - Der er ikke nok ledig plads på hukommelseskortet. -Du skal skabe mindst {} ledig plads for at fortsætte med at bruge programmet. - -Tryk på PS-knappen for at sætte programmet på pause, og slet derefter de andre programmer eller indhold. - Nye gemte data - Lagring udført. - Ønsker du at gemme dataene? - Gemmer... - Gemmer... -Undlad at slukke for systemet eller lukke programmet. - Ønsker du at overskrive disse gemte data? - -
-
- - - Følgende program bliver lukket. - Data, som skal slettes: - - - - Programmet er blevet føjet til hjemmeskærm. - Slet alt - Meddelelserne vil blive slettet. - Kunne ikke installere. - Installation udført. - Installerer... - Der er ingen meddelelsen. - Du har opnået et trophy! - - - - Tilbage - Nu har du udført opstartsopsætningen. -Dit Vita3K-system er klart! - Vælg et sprog. - Næste - - - - Start - Fortsæt - - - - - Standard - - - Navn - Udbyder - Opdateret - Størrelse - Version - - Dette tema vil blive slettet. - - - Billede - - - - - - - ÅÅÅÅ/MM/DD - DD/MM/ÅÅÅÅ - MM/DD/ÅÅÅÅ - - - 12 timers ur - 24 timers ur - - - - Systemsprog - - - - Dansk - Tysk - Engelsk (Storbritannien) - Engelsk (USA) - Spansk - Fransk - Italiensk - Hollandsk - Norsk - Polsk - Portugisisk (Brasilien) - Portugisisk (Portugal) - Russisk - Finsk - Svensk - Tyrkisk - - - - - - - - - - - IP-adresse - Subnetmaske - - Gem & Lukke - - - - Browser - Internetbrowser - Trophies - Trophy Collection - Indstillinger - Indholdsadministrator - - - -
Detaljer
- Opnået - Navn - Der er ingen trophies. -Du kan opnå trophies ved at bruge et program, der understøtter trophy-funkitionen. - Ikke opnået - Original - Sortér - Trophies - Grade - Forløb - Opdaterest -
- - - Vælg bruger - Opret bruger - Den følgende bruger er blevet oprettet: - Navent er allerede i brug. - Slet bruger - Vælg den bruger, som du vil slette. - Følgende bruger vil bilve slettet: - Hvis du sletter brugeren, slettes denne brugers gemte data, trofæer. - Brugeren vil bilve slettet. -Er du sikker på, at du ønsker at fortsætte. - Bruger blev slettet. - Vælg avatar - Bruger - Bekræft - - - - En ny version af Vita3K er tilgængelig. - Tilbage - Ønsker du at afbryde opdateringen? -Hvis du afbryder, vil Vita3K starte download fra dette punkt, næste gang du opdaterer. - Downloader... -Efter download er udført, vil Vita3K automatisk genstarte og derefter installere den nye opdateringen. - Kunne ikke udføre opdateringen. - {} minutter tilbage - Næste - {} sekunder tilbage - Ønsker du at opdatere Vita3K? - Den senere version af Vita3K er allerede installeret. - Den seneste version af Vita3K er allerede installeret. - Nye funktioner i version {} - Opdater - Version {} - -
diff --git a/lang/system/de.xml b/lang/system/de.xml deleted file mode 100644 index 91340cdfc..000000000 --- a/lang/system/de.xml +++ /dev/null @@ -1,413 +0,0 @@ - - - - - - Öffne Präferenzen-Pfad - Installiere Firmware - Installiere .pkg - Installiere .zip, .vpk - Installiere Lizenz - Beenden - - - Zuletzt benutzte Anwendungen - - - Benutzerverwaltung - - - Tastatur - - - Willkommen - - - - - - - - - Handbuch - Aktualisierung - - - Diese Anwendung, sowie alle damit verbundenen Dateien und Nutzerdaten, werden gelöscht. - - Update-Verlauf - Version {} - - berechtigt - nicht berechtigt - Stufe {} - Name - Verdienen von Trophäen - Kindersicherung - Aktualisiert - Größe - Version - Zuletzt benutzt - Nutzungsdauer - Nie - - - - - Ein Fehle ist aufgetreten. -Fehlercode: {} - Abbrechen - Schließen - Laden der Datei gescheitert. - Speichern der Datei gescheitert. - Löschen - Fehler - Die Datei ist beschädigt. - Aktiviere das Mikrofon. - Nein - Warte bitte ... - Alle auswählen - - Ja - - Sonntag - Montag - Dienstag - Mittwoch - Donnerstag - Freitag - Samstag - - - Januar - Februar - März - April - Mai - Juni - Juli - August - September - Oktober - November - Dezember - - - januar - februar - märz - april - mai - juni - juli - august - september - oktober - november - dezember - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1. - 2. - 3. - 4. - 5. - 6. - 7. - 8. - 9. - 10. - 11. - 12. - 13. - 14. - 15. - 16. - 17. - 18. - 19. - 20. - 21. - 22. - 23. - 24. - 25. - 26. - 27. - 28. - 29. - 30. - 31. - - Geheime Trophäe - Vor einer Stunde - Vor einer Minute - Bronze - Gold - Platin - Silber - Vor {} Stunden - Vor {} Minuten - - - - - Die ausgewählten Anwendungen und alle dazugehörigen Daten, einschließlich gespeicherter Daten, werden gelöscht. - Es gibt keine Inhaltsobjekte. - - - The selected saved data items will be deleted. - Es gibt keine gespeicherte Daten. - - Design - Freier Speicherplatz - Alle löschen - - - - Num - - - - - Starten der Anwendung wird vorbereitet ... - - - - Möchtest du den Löschvorgang abbrechen? - Löschen abgeschlossen. - Möchtest du diese gespeicherten Daten löschen? - - -
Einzelheiten
- Aktualisiert -
- - Möchtest du den Ladevorgang abbrechen? - Es gibt keine gespeicherten Daten. - Laden abgeschlossen. - Lädt ... - Möchtest du diese gespeicherten Daten laden? - - - Möchtest du den Speichervorgang abbrechen? - Speichern der Datei gescheitert. -Nicht genügend freier Speicherplatz auf der Speicherkarte. Du musst mindestens {} freien Speicherplatz schaffen, damit du Fortschritte speichern kannst. - -Um freien Speicherplatz zu schaffen, drücke die PS-Taste, um diese Anwendung anzuhalten, und lösche dann andere Anwendungen oder Inhalte. - Nicht genügend freier Speicherplatz auf der Speicherkarte. -Um die Anwendung weiterhin zu verwenden, müssen mindestens {} freier Speicherplatz geschaffen werden. - -Drücke die PS-Taste, um diese Anwendung anzuhalten, und lösche dann andere Anwendungen oder Inhalte. - Neue gespeicherte Daten - Speichern abgeschlossen. - Möchtest du die Daten speichern? - Speichert ... - Speichert ... -Schalte das System nicht aus und schließe die Anwendung nicht. - Möchtest du diese gespeicherten Daten überschreiben? - -
-
- - - Folgende Anwendung wird geschlossen. - Zu löschende Daten: - - - - Die Anwendung wurde dem Startbildschirm hinzugefügt. - Lösche alle - Alle Benachrichtigungen werden gelöscht. - Installation fehlgeschlagen. - Installation abgeschlossen. - Installiere... - Es gibt keine Benachrichtigungen. - Du hast eine Trophäe freigeschaltet! - - - - Zurück - Die Erstinstallation ist jetzt abgeschlossen. -Dein Vita3K-System ist bereit! - Wähle eine Sprache. - Vita3K benötigt vollständigen Dateizugriff. -Tippe auf 'Zugriff gewähren', um fortzufahren. - Zugriff gewähren - Installiere Firmware. - Weiter - - - - Starten - Fortfahren - - - - - Standard - - - Name - Anbieter - Aktualisiert - Größe - Version - Content ID - - Dieses Design wird gelöscht. - - - Bild - - - - - - - JJJJ/MM/TT - TT/MM/JJJJ - MM/TT/JJJJ - - - 12-Stunden-Uhr - 24-Stunden-Uhr - - - - Systemsprache - - - - Dänisch - Deutsch - Englisch (Großbritannien) - Englisch (USA) - Spanisch - Französisch - Italienisch - Niederländisch - Norwegisch - Polnisch - Portugiesisch (Brasilien) - Portugiesisch (Portugal) - Russisch - Finnisch - Schwedisch - Türkisch - - - - - - - - - - - IP-adresse - Subnetzmaske - - Speichern & Schließen - - - - Browser - Internet-Browser - Trophäen - Trophäen-Sammlung - Einstellungen - Inhaltsmanager - - - -
Details
- Verdient - Name - Du hast noch keine Trophäen. -Du kannst Trophäen in Anwendungen verdienen, die die Trophäen-Funktionen unterstützen. - Nicht verdient - Original - Sortierung - Trophäen - Wertigkeit - Fortschritt - Aktualisiert -
- - - Benutzer auswählen - Benutzer erstellen - Der folgende Benutzer wurde erstellt. - Benutzer bearbeiten - Dieser Name wird bereits verwendet. - Benutzer löschen - Wähle den Benutzer aus, den löschen möchtest. - Der folgende Benutzer wird gelöscht. - Wenn du den Benutzer löschst, werden die gespeicherten Daten und Trophäen dieses Benutzer gelöscht. - Der Benutzer wird gelöscht. -Wirklich fortfahren? - Benutzer gelöscht. - Avatar auswählen - Avatar zurücksetzen - Benutzer - Bestätigen - Benutzer automatisch einloggen - - - - Es ist eine neue Version der Vita3K verfügbar. - Zurück - Möchtest du die Aktualisierung abbrechen? -Wenn du abbrichst, startet das Vita3K den Download an dieser Stelle beim nächsten Aktualiseren neu. - Download läuft ... -Nach dem Herunterladen wird Vita3K automatisch neu gestartet und die neue Aktualisierung installiert. - Aktualisierung konnte nicht abgeschlossen werden. - Noch {} Minuten - Weiter - Noch {} Sekunden - Möchtest du Vita3K aktualisieren? - Es ist bereits eine höhere Version der Vita3K installiert. - Es ist bereits die aktuellste Version der Vita3K installiert. - Neue Funktionen in Version {} - Authors - Comments - Aktualisieren - Version {} - -
diff --git a/lang/system/en-gb.xml b/lang/system/en-gb.xml deleted file mode 100644 index f83c154c0..000000000 --- a/lang/system/en-gb.xml +++ /dev/null @@ -1,998 +0,0 @@ - - - - - - Open Pref Path - Open Patch Path - Open Textures Path - Install Firmware - Install .pkg - Install .zip, .vpk - Install License - Exit - - - Last Apps used - Empty - - - Threads - Semaphores - Mutexes - Lightweight Mutexes - Condition Variables - Lightweight Condition Variables - Event Flags - Memory Allocations - Disassembly - - - User Management - - - Keyboard Controls - - - Welcome - - - - - Vita3K: a PS Vita/PS TV Emulator. The world's first functional PS Vita/PS TV emulator. - Vita3K is an experimental open-source PlayStation Vita/PlayStation TV emulator written in C++ for Windows, Linux, macOS and Android operating systems. - Special credit: The Vita3K icon was designed by: - If you're interested in contributing, check out our: - Visit our website for more info: - If you want to support us, you can make a donation or subscribe to our: - Vita3K Staff - Developers - Contributors - Supporters - - - - - Check App state - Copy Vita3K Summary - Open state report - Create state report - Update database - - - Name and Title ID - App Summary - - Create Shortcut - - Create - Edit - Remove - - - Open Folder - Patch - DLC - License - Shaders Cache - Shaders Log - - - Manual - Update - - - This application and all related data, including saved data, will be deleted. - Deleting an application may take a while, -depending on its size and your hardware. - Do you want to delete this patch? - Do you want to delete this add-on data? - Do you want to delete this license? - - - Reset Last Time Played - - Update History - Version {} - - Eligible - Ineligible - Level {} - Name - Trophy Earning - Parental Controls - Updated - Size - Version - Title ID - Category - Game Digital Application - Game Patch - Last time used - Time used - Never - - - {}s - {}m:{}s - {}h:{}m:{}s - {}d:{}h:{}m:{}s - {}w:{}d:{}h:{}m:{}s - - - - - An error occurred. -Error code: {} - Cancel - Close - Could not load the file. - Could not save the file. - Delete - Error - The file is corrupt. - Enable the microphone. - No - OK - Please wait... - Search - Select All - - Submit - Yes - - Sunday - Monday - Tuesday - Wednesday - Thursday - Friday - Saturday - - - January - February - March - April - May - June - July - August - September - October - November - December - - - january - february - march - april - may - june - july - august - september - october - november - december - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Hidden Trophy - 1 Hour Ago - 1 Minute Ago - Bronze - Gold - Platinum - Silver - {} Hours Ago - {} Minutes Ago - - - - - Unknown - Nothing - Bootable - Intro - Menu - Ingame - - Ingame + - Playable - - - - - Failed to get current compatibility database, check firewall/internet access, try again later. - Failed to download Applications compatibility database updated at: {}, try again later. - Failed to load Applications compatibility database downloaded updated at: {}. - The compatibility database was successfully updated from {} to {}. - -{} new application(s) are listed, bringing the total to {}! - The compatibility database was successfully updated from {} to {}. - -{} applications are listed! - The compatibility database updated at {} has been successfully downloaded and loaded. - -{} applications are listed! - - - - Compiling Shaders - {} pipelines compiled - {} shaders compiled - - - - - The selected applications and all related data, including saved data, will be deleted. - There are no content items. - - - The selected saved data items will be deleted. - There is no saved data. - - Themes - Free Space - Clear all - - - - {} controllers connected - Num - Motion Support - Rebind Controls - LED Color - Use Custom Color - Check this box to use custom color for the controller's LED. - Red - Green - Blue - No compatible controllers connected. -Connect a controller that is compatible with SDL3. - Disable Motion - Using builtin device motion sensors - Reset Controller Binding - - - - - Mapped button - Left stick up - Left stick down - Left stick right - Left stick left - Right stick up - Right stick down - Right stick right - Right stick left - D-pad up - D-pad down - D-pad right - D-pad left - Square button - Cross button - Circle button - Triangle button - Start button - Select button - PS button - L1 button - R1 button - Only in PS TV mode - L2 button - R2 button - L3 button - R3 button - GUI - Full Screen - Toggle Touch - Toggles between back touch and screen touch. - Toggle GUI Visibility - Toggles between showing and hiding the GUI at the top of the screen while the app is running. - Miscellaneous - Toggle Texture Replacement - Take A Screenshot - Pinch modifier - Alternate pinch in key - Alternate pinch out/stretch key - The key is used for other bindings or it is reserved. - Reset Controls Binding - - - - - Preparing to start the application... - - - - Do you want to cancel deleting? - Deletion complete. - Do you want to delete this saved data? - - -
Details
- Updated -
- - Do you want to cancel loading? - Could not load the file. - There is no saved data. - Loading complete. - Loading... - Do you want to load this saved data? - - - Do you want to cancel saving? - Could not save the file. -There is not enough free space on the memory card. To save your progress in the application, you must create at least {} of free space. - -To create the free space, press PS button to pause this application, and then delete other applications or content. - There is not enough free space on the memory card. -To continue using the application, you must create at least {} of free space. - -Press the PS button to pause this application, and then delete other applications or content. - New Saved Data - Saving complete. - Do you want to save the data? - Saving... - Saving... -Do not power off the system or close the application. - Do you want to overwrite this saved data? - -
-
- - - The following application will close. - Data to be Deleted: - - - - Filter - Sort Apps By - All - By Region - USA - Europe - Japan - Asia - By Type - Commercial - Homebrew - By Compatibility State - Ver - Cat - Comp - Last Time - Refresh - - - - The application has been added to the home screen. - Delete All - The notifications will be deleted. - Could not install. - Installation complete. - Installing... - There are no notifications. - You have earned a trophy! - - - - Back - You have now completed initial setup. -Your Vita3K system is ready! - Select a language. - Select a pref path. - Vita3K requires full file access. -Tap 'Grant Access' to continue. - Grant Access - Current emulator path - Change Emulator Path - Reset Emulator Path - Install Firmware. - Installing all firmware files is highly recommended. - Installed: - Download Font Package - Install Firmware File - Select interface settings. - Info Bar Visible - Check the box to show info bar inside app selector. -Info bar is clock, battery level and notification center. - Live Area App Screen - Check the box to open Live Area by default when clicking on an application. -If disabled, right click on an application to open it. - Grid Mode - Check the box to set the app list to grid mode like of PS Vita. - App Icon Size - Select your preferred icon size. - Completed. - Next - - - - - Firmware Installation - Installation in progress, please wait... - Firmware successfully installed. - Firmware version: - No firmware font package present, please download and install it. - Firmware font package is needed for some applications -and also for Asian regional font support. (Generally Recommended) - Delete the firmware installation file? - - - Select license type - Select work.bin/rif - Enter zRIF - Enter zRIF key - Please input your zRIF here - Ctrl + C to copy, Ctrl + V to paste. - Delete the pkg file? - Delete the work.bin/rif file? - Failed to install package. -Check pkg and work.bin/rif file or zRIF key. - - - Select install type - Select File - Select Directory - {} archive(s) found with compatible contents. - {} archive(s) contents successfully installed: - Update App to: - Failed to install {} archive(s) contents: - No compatible content found in {} archive(s): - Delete archive? - - - Successfully installed license. - Failed to install license. -Check work.bin/rif file or zRIF key. - - - Reinstall this content? - This content is already installed. - Do you want to reinstall it and overwrite existing data? - - - - - Start - Continue - - Using configuration set for keyboard in control setting - Firmware not detected. Installation is highly recommended - Firmware font not detected. Installing it is recommended for font text in Live Area - Live Area Help - Browse in app list - D-pad, Left Stick, Wheel in Up/Down or using Slider - Start App - Click on Start or Press on Cross - Show/Hide Live Area during app run - Press on PS - Exit Live Area - Click on Esc or Press on Circle - Manual Help - Browse page - D-pad, Left Stick, Wheel in Up/Down or using Slider, Click on </> - Hide/Show button - Left Click or Press on Triangle - Exit Manual - Click on Esc or Press on PS - - - - - Failed to load "{}". -Check vita3k.log to see console output for details. -1. Have you installed the firmware? -2. Re-dump your own PS Vita app/game and install it on Vita3K. -3. If you want to install or boot Vitamin, it is not supported. - - - - Gamepad Overlay - Show gamepad overlay ingame - Hide Gamepad Overlay - Modify Gamepad Overlay - Overlay Scale - Overlay Opacity - Reset Gamepad - Show front/back touchscreen switch button. - L2/R2 triggers will be displayed only if PSTV mode is enabled. - - - - Avg - Min - Max - - - - - System Music - - - Default - - Find a PSVita Custom Themes - - Name - Provider - Updated - Size - Version - Content ID - - This theme will be deleted. - - - Image - Add Image - - - Delete Background - Add Background - - - - - YYYY/MM/DD - DD/MM/YYYY - MM/DD/YYYY - - - 12-Hour Clock - 24-Hour Clock - - - - System Language - - - - Danish - German - English (United Kingdom) - English (United States) - Spanish - French - Italian - Dutch - Norwegian - Polish - Portuguese (Brazil) - Portuguese (Portugal) - Russian - Finnish - Swedish - Turkish - - - - - - - - - Modules Mode - Automatic - Select Automatic mode to use a preset list of modules. - Auto & Manual - Select this mode to load Automatic module and selected modules from the list below. - Manual - Select Manual mode to load selected modules from the list below. - Modules List - Select your desired modules. - Search Modules - Clear List - No modules present. -Please download and install the last PS Vita firmware. - Refresh List - - - Enable optimizations - Check the box to enable additional CPU JIT optimizations. - - - Reset - Backend Renderer - Select your preferred backend renderer. - GPU (Reboot to apply) - Select the GPU Vita3K should run on. - Add custom driver - Download custom driver - Remove custom driver - Standard - High - Renderer Accuracy - V-Sync - Disabling V-Sync can fix the speed issue in some games. -It is recommended to keep it enabled to avoid visual tearing. - Disable Surface Sync - Speed hack, check the box to disable surface syncing between CPU and GPU. -Surface syncing is needed by a few games. -Gives a big performance boost if disabled (in particular when upscaling is on). - Asynchronous Pipeline Compilation - Allow pipelines to be compiled concurrently on multiple concurrent threads. -This decreases pipeline compilation stutter at the cost of temporary graphical glitches. - Nearest - Bilinear - Bicubic - Screen Filter - Set post-processing filter to apply. - Internal Resolution Upscaling - Enable upscaling for Vita3K. -Experimental: games are not guaranteed to render properly at more than 1x. - Anisotropic Filtering - Anisotropic filtering is a technique to enhance the image quality of surfaces -which are sloped relative to the viewer. -It has no drawback but can impact performance. - Texture Replacement - Export Textures - Import Textures - Texture Exporting Format - FPS Hack - Game hack which allows some games running at 30 FPS to run at 60 FPS on the emulator. -Note that this is a hack and will only work on some games. -On other games, it may have no effect or make them run twice as fast. - Disabled - Double buffer - External host - Page table - Native buffer - Memory mapping method - Memory mapping improved performances, reduces memory usage and fixes many graphical issues. -However, it may be unstable on some GPUs. - Enable Turbo Mode - Provides a way to force the GPU to run at the maximum possible clocks (thermal constraints will still be applied). - Shaders - Use Shader Cache - Check the box to enable shader cache to pre-compile it at game startup. -Uncheck to disable this feature. - Use Spir-V Shader (deprecated) - Pass generated Spir-V shader directly to driver. -Note that some beneficial extensions will be disabled, -and not all GPUs are compatible with this. - Clean Shaders Cache and Log - - - - Enter button assignment -Select your 'Enter' button. - This is the button that is used as 'Confirm' in applications dialogs. -Some applications don't use this and get default confirmation button. - Circle - Cross - PS TV Mode - Check the box to enable PS TV Emulated mode. - Show Mode - Check the box to enable Show mode. - Demo Mode - Check the box to enable Demo mode. - - - Boot apps in full screen - Trace - Info - Warning - Error - Critical - Off - Log Level - Select your preferred log level. - Archive Log - Check the box to enable Archive Log. - Enables Discord Rich Presence to show what application you're running on Discord. - Texture Cache - Uncheck the box to disable texture cache. - Show Compile Shaders - Uncheck the box to disable the display of the compile shaders dialog. - Show Touchpad Cursor - Uncheck the box to disable showing the touchpad cursor on-screen. - Log Compatibility Warning - Check the box to enable log compatibility warning of GitHub issue. - Check For Updates - Automatically check for updates at startup. - File Loading Delay - File loading delay in milliseconds. -This is required for some games to load files too quickly compared to real hardware (e.g., Silent Hill). -Default is 0 ms. - Performance Overlay - Display performance information on the screen as an overlay. - Minimum - Low - Medium - Maximum - Detail - Select your preferred performance overlay detail. - Top Left - Top Center - Top Right - Bottom Left - Bottom Center - Bottom Right - Position - Select your preferred performance overlay position. - Check to enable case-insensitive path finding on case sensitive filesystems. -RESETS ON RESTART - Allows emulator to attempt searching for files regardless of case -on non-Windows platforms. - Emulated System Storage Folder - Current emulator path: - Change Emulator Path - Change Vita3K emulator folder path. -You will need to move your old folder to the new location manually. - Reset Emulator Path - Reset Vita3K emulator path to the default. -You will need to move your old folder to the new location manually. - Custom Config Settings - Clear Custom Config - Screenshot Image Type - NULL - Screenshot Format - - - GUI Visible - Check the box to show GUI after booting an application. - Info Bar Visible - Check the box to show an info bar inside the app selector. - GUI Language - Select your user language. - Display Info Message - Uncheck the box to display info message in log only. - Display System Apps - Uncheck the box to disable the display of system apps on the home screen. -They will be shown in the main menu bar only. - Live Area App Screen - Check the box to open the Live Area by default when clicking on an application. -If disabled, right click on an application to open it. - Stretch The Display Area - Check the box to enlarge the display area to fit the screen size. - Fullscreen HD res pixel perfect - Check the box to get a pixel perfect rendering with HD resolutions (1080p, 4K) in fullscreen on a 16:9 aspect ratio monitor -at the price of slight cropping at the top and the bottom of the screen. - Grid Mode - Check the box to set the app list to grid mode. - App Icon Size - Select your preferred icon size. - Font support - Asia Region - Check this box to enable font support for Chinese and Korean. -Enabling this will use more memory and will require you to restart the emulator. - Firmware font package is needed for some applications -and also for Asian regional font support in GUI. -It is also generally recommended for GUI. - Theme & Background - Current theme content id: - Reset Default Theme - Using theme background - Clean User Backgrounds - Current start background: - Reset Start Background - Background Alpha - Select your preferred background transparency. -The minimum is opaque and the maximum is transparent. - Delay for backgrounds - Select the delay (in seconds) before changing backgrounds. - Delay for start screen - Select the delay (in seconds) before returning to the start screen. - - - Signed in PSN - If checked, games will consider the user is connected to the PSN network (but offline). - IP Address - Select the IP address to be used in adhoc. - Subnet Mask - Enable HTTP - Check this box to enable games to use the HTTP protocol on the internet. - HTTP Timeout Attempts - How many attempts to do when the server doesn't respond. -Could be useful if you have very unstable or VERY SLOW internet. - HTTP Timeout Sleep - Attempt sleep time when the server doesn't answer. -Could be useful if you have very unstable or VERY SLOW internet. - HTTP Read End Attempts - How many attempts to do when there isn't more data to read, -lower can improve performance but can make games unstable if you have bad enough internet. - HTTP Read End Sleep - Attempt sleep time when there isn't more data to read, -lower can improve performance but can make games unstable if you have bad enough internet. - - - Import logging - Log module import symbols. - Export logging - Log module export symbols. - Shader logging - Log shaders being used on each draw call. - Uniform logging - Log shader uniform names and values. - Save color surfaces - Save color surfaces to files. - ELF dumping - Dump loaded code as ELFs. - Validation Layer (Reboot required) - Enable Vulkan validation layer. - Unwatch Code - Watch Code - Unwatch Memory - Watch Memory - Unwatch Import Calls - Watch Import Calls - - Save & Reboot - Save & Apply - Save & Close - Click on Save to keep your changes. - - - - Browser - Internet Browser - Trophies - Trophy Collection - Settings - Content Manager - - - - All trophy information saved on this user will be deleted. - Delete Trophy - This trophy information saved on this user will be deleted. - Locked -
Details
- Earned - Name - There are no trophies. -You can earn trophies by using an application that supports the trophy feature. - Not Earned - Original - Sort - Trophies - Grade - Progress - Updated - Advance - Show Hidden Trophies -
- - - Select User - Create User - The following user has been created. - Edit User - Open User Folder - This name is already in use. - Delete User - Select the user you want to delete. - The following user will be deleted. - If you delete the user, that user's saved data, trophies will be deleted. - The user will be deleted. -Are you sure you want to continue? - User deleted. - Choose Avatar - Reset Avatar - User - Confirm - Automatic User Login - - - - A new version of Vita3K is available. - Back - Do you want to cancel the update? -If you cancel, the next time you update, Vita3K will start downloading from this point. - Downloading... -After the download is complete, Vita3K will restart automatically and then install the new update. - Could not complete the update. - {} Minutes Left - Next - {} Seconds Left - Do you want to update Vita3K? - The later version of Vita3K is already installed. - The latest version of Vita3K is already installed. - New Features in Version {} - Authors - Comments - Update - Version {} - - - - Vita3K PlayStation Vita Emulator - Vita3K is an open-source PlayStation Vita emulator written in C++ for Windows, Linux, macOS and Android. - The emulator is still in its development stages so any feedback and testing is greatly appreciated. - To get started, please install all PS Vita firmware files. - Download Preinst Firmware - Download Firmware - Download Firmware Font Package - A comprehensive guide on how to set-up Vita3K can be found on the - Quickstart - page. - Consult the Commercial game and the Homebrew compatibility list to see what currently runs. - Commercial Compatibility List - Homebrew Compatibility List - Contributions are welcome! - Additional support can be found in the #help channel of the - Vita3K does not condone piracy. You must dump your own games. - Show next time - -
diff --git a/lang/system/en.xml b/lang/system/en.xml deleted file mode 100644 index 948e7ed48..000000000 --- a/lang/system/en.xml +++ /dev/null @@ -1,1006 +0,0 @@ - - - - - - Open Pref Path - Open Patch Path - Open Textures Path - Install Firmware - Install .pkg - Install .zip, .vpk - Install License - Exit - - - Last Apps used - Empty - - - Threads - Semaphores - Mutexes - Lightweight Mutexes - Condition Variables - Lightweight Condition Variables - Event Flags - Memory Allocations - Disassembly - - - User Management - - - Keyboard Controls - - - Welcome - - - - - Vita3K: a PS Vita/PS TV Emulator. The world's first functional PS Vita/PS TV emulator. - Vita3K is an experimental open-source PlayStation Vita/PlayStation TV emulator written in C++ for Windows, Linux, macOS and Android operating systems. - Special credit: The Vita3K icon was designed by: - If you're interested in contributing, check out our: - Visit our website for more info: - If you want to support us, you can make a donation or subscribe to our: - Vita3K Staff - Developers - Contributors - Supporters - - - - - Check App state - Copy Vita3K Summary - Open state report - Create state report - Update database - - - Name and Title ID - App Summary - - Create Shortcut - - Create - Edit - Remove - - - Open Folder - Patch - DLC - License - Shaders Cache - Shaders Log - - - Manual - Update - - - This application and all related data, including saved data, will be deleted. - Deleting an application may take a while, -depending on its size and your hardware. - Do you want to delete this patch? - Do you want to delete this add-on data? - Do you want to delete this license? - - - Reset Last Time Played - - Update History - Version {} - - Eligible - Ineligible - Level {} - Name - Trophy Earning - Parental Controls - Updated - Size - Version - Title ID - Category - Game Digital Application - Game Patch - Last time used - Time used - Never - - - {}s - {}m:{}s - {}h:{}m:{}s - {}d:{}h:{}m:{}s - {}w:{}d:{}h:{}m:{}s - - - - - An error occurred. -Error code: {} - Cancel - Close - Could not load the file. - Could not save the file. - Delete - Error - The file is corrupt. - Enable the microphone. - No - OK - Please wait... - Search - Select All - - Submit - Yes - - Sunday - Monday - Tuesday - Wednesday - Thursday - Friday - Saturday - - - January - February - March - April - May - June - July - August - September - October - November - December - - - january - february - march - april - may - june - july - august - september - october - november - december - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Hidden Trophy - 1 Hour Ago - 1 Minute Ago - Bronze - Gold - Platinum - Silver - {} Hours Ago - {} Minutes Ago - - - - - Unknown - Nothing - Bootable - Intro - Menu - Ingame - - Ingame + - Playable - - - - - Failed to get current compatibility database, check firewall/internet access, try again later. - Failed to download Applications compatibility database updated at: {}, try again later. - Failed to load Applications compatibility database downloaded updated at: {}. - The compatibility database was successfully updated from {} to {}. - -{} new application(s) are listed, bringing the total to {}! - The compatibility database was successfully updated from {} to {}. - -{} applications are listed! - The compatibility database updated at {} has been successfully downloaded and loaded. - -{} applications are listed! - - - - Compiling Shaders - {} pipelines compiled - {} shaders compiled - - - - - The selected applications and all related data, including saved data, will be deleted. - There are no content items. - - - The selected saved data items will be deleted. - There is no saved data. - - Themes - Free Space - Clear all - - - - {} controllers connected - Num - Motion Support - Rebind Controls - LED Color - Use Custom Color - Check this box to use custom color for the controller's LED. - Red - Green - Blue - No compatible controllers connected. -Connect a controller that is compatible with SDL3. - Disable Motion - Using builtin device motion sensors - Reset Controller Binding - - - - - Mapped button - Alternate mapped button - Left stick up - Left stick down - Left stick right - Left stick left - Right stick up - Right stick down - Right stick right - Right stick left - D-pad up - D-pad down - D-pad right - D-pad left - Square button - Cross button - Circle button - Triangle button - Start button - Select button - PS button - L1 button - R1 button - Only in PS TV mode - L2 button - R2 button - L3 button - R3 button - GUI - Full Screen - Toggle Touch - Toggles between back touch and screen touch. - Toggle GUI Visibility - Toggles between showing and hiding the GUI at the top of the screen while the app is running. - Miscellaneous - Toggle Texture Replacement - Take A Screenshot - Pinch modifier - Alternate pinch in key - Alternate pinch out/stretch key - The key is used for other bindings or it is reserved. - Reset Controls Binding - - - - - Preparing to start the application... - - - - Do you want to cancel deleting? - Deletion complete. - Do you want to delete this saved data? - - -
Details
- Updated -
- - Do you want to cancel loading? - There is no saved data. - Loading complete. - Loading... - Do you want to load this saved data? - - - Do you want to cancel saving? - Could not save the file. -There is not enough free space on the memory card. To save your progress in the application, you must create at least {} of free space. - -To create the free space, press PS button to pause this application, and then delete other applications or content. - There is not enough free space on the memory card. -To continue using the application, you must create at least {} of free space. - -Press the PS button to pause this application, and then delete other applications or content. - New Saved Data - Saving complete. - Do you want to save the data? - Saving... - Saving... -Do not power off the system or close the application. - Do you want to overwrite this saved data? - -
-
- - - The following application will close. - Data to be Deleted: - - - - Filter - Sort Apps By - All - By Region - USA - Europe - Japan - Asia - By Type - Commercial - Homebrew - By Compatibility State - Ver - Cat - Comp - Last Time - Refresh - - - - The application has been added to the home screen. - Delete All - The notifications will be deleted. - Could not install. - Installation complete. - Installing... - There are no notifications. - You have earned a trophy! - - - - Back - You have now completed initial setup. -Your Vita3K system is ready! - Select a language. - Select a pref path. - Vita3K requires full file access. -Tap 'Grant Access' to continue. - Grant Access - Current emulator path - Change Emulator Path - Reset Emulator Path - Install Firmware. - Installing all firmware files is highly recommended. - Installed: - Download Font Package - Install Firmware File - Select interface settings. - Info Bar Visible - Check the box to show info bar inside app selector. -Info bar is clock, battery level and notification center. - Live Area App Screen - Check the box to open Live Area by default when clicking on an application. -If disabled, right click on an application to open it. - Grid Mode - Check the box to set the app list to grid mode like of PS Vita. - App Icon Size - Select your preferred icon size. - Completed. - Next - - - - - Firmware Installation - Installation in progress, please wait... - Firmware successfully installed. - Firmware version: - No firmware font package present, please download and install it. - Firmware font package is needed for some applications -and also for Asian regional font support. (Generally Recommended) - Delete the firmware installation file? - - - Select license type - Select work.bin/rif - Enter zRIF - Enter zRIF key - Please input your zRIF here - Ctrl + C to copy, Ctrl + V to paste. - Delete the pkg file? - Delete the work.bin/rif file? - Failed to install package. -Check pkg and work.bin/rif file or zRIF key. - - - Select install type - Select File - Select Directory - {} archive(s) found with compatible contents. - {} archive(s) contents successfully installed: - Update App to: - Failed to install {} archive(s) contents: - No compatible content found in {} archive(s): - Delete archive? - - - Successfully installed license. - Failed to install license. -Check work.bin/rif file or zRIF key. - - - Reinstall this content? - This content is already installed. - Do you want to reinstall it and overwrite existing data? - - - - - Start - Continue - - Using configuration set for keyboard in control setting - Firmware not detected. Installation is highly recommended - Firmware font not detected. Installing it is recommended for font text in Live Area - Live Area Help - Browse in app list - D-pad, Left Stick, Wheel in Up/Down or using Slider - Start App - Click on Start or Press on Cross - Show/Hide Live Area during app run - Press on PS - Exit Live Area - Click on Esc or Press on Circle - Manual Help - Browse page - D-pad, Left Stick, Wheel in Up/Down or using Slider, Click on </> - Hide/Show button - Left Click or Press on Triangle - Exit Manual - Click on Esc or Press on PS - - - - - Failed to load "{}". -Check vita3k.log to see console output for details. -1. Have you installed the firmware? -2. Re-dump your own PS Vita app/game and install it on Vita3K. -3. If you want to install or boot Vitamin, it is not supported. - - - - Gamepad Overlay - Show gamepad overlay ingame - Hide Gamepad Overlay - Modify Gamepad Overlay - Overlay Scale - Overlay Opacity - Reset Gamepad - Show front/back touchscreen switch button. - L2/R2 triggers will be displayed only if PSTV mode is enabled. - - - - Avg - Min - Max - - - - - System Music - - - Default - - Find a PSVita Custom Themes - - Name - Provider - Updated - Size - Version - Content ID - - This theme will be deleted. - - - Image - Add Image - - - Delete Background - Add Background - - - - - YYYY/MM/DD - DD/MM/YYYY - MM/DD/YYYY - - - 12-Hour Clock - 24-Hour Clock - - - - System Language - - - - Danish - German - English (United Kingdom) - English (United States) - Spanish - French - Italian - Dutch - Norwegian - Polish - Portuguese (Brazil) - Portuguese (Portugal) - Russian - Finnish - Swedish - Turkish - - - - - - - - - Modules Mode - Automatic - Select Automatic mode to use a preset list of modules. - Auto & Manual - Select this mode to load Automatic module and selected modules from the list below. - Manual - Select Manual mode to load selected modules from the list below. - Modules List - Select your desired modules. - Search Modules - Clear List - No modules present. -Please download and install the last PS Vita firmware. - Refresh List - - - Enable optimizations - Check the box to enable additional CPU JIT optimizations. - - - Reset - Backend Renderer - Select your preferred backend renderer. - GPU (Reboot to apply) - Select the GPU Vita3K should run on. - Add custom driver - Download custom driver - Remove custom driver - Standard - High - Renderer Accuracy - V-Sync - Disabling V-Sync can fix the speed issue in some games. -It is recommended to keep it enabled to avoid visual tearing. - Disable Surface Sync - Speed hack, check the box to disable surface syncing between CPU and GPU. -Surface syncing is needed by a few games. -Gives a big performance boost if disabled (in particular when upscaling is on). - Asynchronous Pipeline Compilation - Allow pipelines to be compiled concurrently on multiple concurrent threads. -This decreases pipeline compilation stutter at the cost of temporary graphical glitches. - Nearest - Bilinear - Bicubic - Screen Filter - Set post-processing filter to apply. - Internal Resolution Upscaling - Enable upscaling for Vita3K. -Experimental: games are not guaranteed to render properly at more than 1x. - Anisotropic Filtering - Anisotropic filtering is a technique to enhance the image quality of surfaces -which are sloped relative to the viewer. -It has no drawback but can impact performance. - Texture Replacement - Export Textures - Import Textures - Texture Exporting Format - FPS Hack - Game hack which allows some games running at 30 FPS to run at 60 FPS on the emulator. -Note that this is a hack and will only work on some games. -On other games, it may have no effect or make them run twice as fast. - Disabled - Double buffer - External host - Page table - Native buffer - Memory mapping method - Memory mapping improved performances, reduces memory usage and fixes many graphical issues. -However, it may be unstable on some GPUs. - Enable Turbo Mode - Provides a way to force the GPU to run at the maximum possible clocks (thermal constraints will still be applied). - Shaders - Use Shader Cache - Check the box to enable shader cache to pre-compile it at game startup. -Uncheck to disable this feature. - Use Spir-V Shader (deprecated) - Pass generated Spir-V shader directly to driver. -Note that some beneficial extensions will be disabled, -and not all GPUs are compatible with this. - Clean Shaders Cache and Log - - - - Front Camera - Back Camera - Image not set - Set image - Solid Color - Static Image - - - Enter button assignment -Select your 'Enter' button. - This is the button that is used as 'Confirm' in applications dialogs. -Some applications don't use this and get default confirmation button. - Circle - Cross - PS TV Mode - Check the box to enable PS TV Emulated mode. - Show Mode - Check the box to enable Show mode. - Demo Mode - Check the box to enable Demo mode. - - - Boot apps in full screen - Trace - Info - Warning - Error - Critical - Off - Log Level - Select your preferred log level. - Archive Log - Check the box to enable Archive Log. - Enables Discord Rich Presence to show what application you're running on Discord. - Texture Cache - Uncheck the box to disable texture cache. - Show Compile Shaders - Uncheck the box to disable the display of the compile shaders dialog. - Show Touchpad Cursor - Uncheck the box to disable showing the touchpad cursor on-screen. - Log Compatibility Warning - Check the box to enable log compatibility warning of GitHub issue. - Check For Updates - Automatically check for updates at startup. - File Loading Delay - File loading delay in milliseconds. -This is required for some games to load files too quickly compared to real hardware (e.g., Silent Hill). -Default is 0 ms. - Performance Overlay - Display performance information on the screen as an overlay. - Minimum - Low - Medium - Maximum - Detail - Select your preferred performance overlay detail. - Top Left - Top Center - Top Right - Bottom Left - Bottom Center - Bottom Right - Position - Select your preferred performance overlay position. - Check to enable case-insensitive path finding on case sensitive filesystems. -RESETS ON RESTART - Allows emulator to attempt searching for files regardless of case -on non-Windows platforms. - Emulated System Storage Folder - Current emulator path: - Change Emulator Path - Change Vita3K emulator folder path. -You will need to move your old folder to the new location manually. - Reset Emulator Path - Reset Vita3K emulator path to the default. -You will need to move your old folder to the new location manually. - Custom Config Settings - Clear Custom Config - Screenshot Image Type - NULL - Screenshot Format - - - GUI Visible - Check the box to show GUI after booting an application. - Info Bar Visible - Check the box to show an info bar inside the app selector. - GUI Language - Select your user language. - Display Info Message - Uncheck the box to display info message in log only. - Display System Apps - Uncheck the box to disable the display of system apps on the home screen. -They will be shown in the main menu bar only. - Live Area App Screen - Check the box to open the Live Area by default when clicking on an application. -If disabled, right click on an application to open it. - Stretch The Display Area - Check the box to enlarge the display area to fit the screen size. - Fullscreen HD res pixel perfect - Check the box to get a pixel perfect rendering with HD resolutions (1080p, 4K) in fullscreen on a 16:9 aspect ratio monitor -at the price of slight cropping at the top and the bottom of the screen. - Grid Mode - Check the box to set the app list to grid mode. - App Icon Size - Select your preferred icon size. - Font support - Asia Region - Check this box to enable font support for Chinese and Korean. -Enabling this will use more memory and will require you to restart the emulator. - Firmware font package is needed for some applications -and also for Asian regional font support in GUI. -It is also generally recommended for GUI. - Theme & Background - Current theme content id: - Reset Default Theme - Using theme background - Clean User Backgrounds - Current start background: - Reset Start Background - Background Alpha - Select your preferred background transparency. -The minimum is opaque and the maximum is transparent. - Delay for backgrounds - Select the delay (in seconds) before changing backgrounds. - Delay for start screen - Select the delay (in seconds) before returning to the start screen. - - - Signed in PSN - If checked, games will consider the user is connected to the PSN network (but offline). - IP Address - Select the IP address to be used in adhoc. - Subnet Mask - Enable HTTP - Check this box to enable games to use the HTTP protocol on the internet. - HTTP Timeout Attempts - How many attempts to do when the server doesn't respond. -Could be useful if you have very unstable or VERY SLOW internet. - HTTP Timeout Sleep - Attempt sleep time when the server doesn't answer. -Could be useful if you have very unstable or VERY SLOW internet. - HTTP Read End Attempts - How many attempts to do when there isn't more data to read, -lower can improve performance but can make games unstable if you have bad enough internet. - HTTP Read End Sleep - Attempt sleep time when there isn't more data to read, -lower can improve performance but can make games unstable if you have bad enough internet. - - - Import logging - Log module import symbols. - Export logging - Log module export symbols. - Shader logging - Log shaders being used on each draw call. - Uniform logging - Log shader uniform names and values. - Save color surfaces - Save color surfaces to files. - ELF dumping - Dump loaded code as ELFs. - Validation Layer (Reboot required) - Enable Vulkan validation layer. - Unwatch Code - Watch Code - Unwatch Memory - Watch Memory - Unwatch Import Calls - Watch Import Calls - - Save & Reboot - Save & Apply - Save & Close - Click on Save to keep your changes. - - - - Browser - Internet Browser - Trophies - Trophy Collection - Settings - Content Manager - - - - All trophy information saved on this user will be deleted. - Delete Trophy - This trophy information saved on this user will be deleted. - Locked -
Details
- Earned - Name - There are no trophies. -You can earn trophies by using an application that supports the trophy feature. - Not Earned - Original - Sort - Trophies - Grade - Progress - Updated - Advance - Show Hidden Trophies -
- - - Select User - Create User - The following user has been created. - Edit User - Open User Folder - This name is already in use. - Delete User - Select the user you want to delete. - The following user will be deleted. - If you delete the user, that user's saved data, trophies will be deleted. - The user will be deleted. -Are you sure you want to continue? - User deleted. - Choose Avatar - Reset Avatar - User - Confirm - Automatic User Login - - - - A new version of Vita3K is available. - Back - Do you want to cancel the update? -If you cancel, the next time you update, Vita3K will start downloading from this point. - Downloading... -After the download is complete, Vita3K will restart automatically and then install the new update. - Could not complete the update. - {} Minutes Left - Next - {} Seconds Left - Do you want to update Vita3K? - The later version of Vita3K is already installed. - The latest version of Vita3K is already installed. - New Features in Version {} - Authors - Comments - Update - Version {} - - - - Vita3K PlayStation Vita Emulator - Vita3K is an open-source PlayStation Vita emulator written in C++ for Windows, Linux, macOS and Android. - The emulator is still in its development stages so any feedback and testing is greatly appreciated. - To get started, please install all PS Vita firmware files. - Download Preinst Firmware - Download Firmware - Download Firmware Font Package - A comprehensive guide on how to set-up Vita3K can be found on the - Quickstart - page. - Consult the Commercial game and the Homebrew compatibility list to see what currently runs. - Commercial Compatibility List - Homebrew Compatibility List - Contributions are welcome! - Additional support can be found in the #help channel of the - Vita3K does not condone piracy. You must dump your own games. - Show next time - -
diff --git a/lang/system/es.xml b/lang/system/es.xml deleted file mode 100644 index f8e8fd06c..000000000 --- a/lang/system/es.xml +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - Abrir la Carpeta Pref - Instalar Firmware - Instalar un .pkg - Instalar un .zip, .vpk - Instalar una licencia - Salir - - - Últimas aplicaciones utilizadas - - - Gestión de usuarios - - - Controles del teclado - - - Bienvenido - - - - - Desarrolladores - Ayudantes - - - - - Nombre y título ID - - - Hacer - Editar - Quitar - - - Manual - Actualización - - - Se eliminarán la aplicación y todos los datos relacionados, incluidos los datos guardados. - - Historial de actualizaciones - Versión {} - - Elegible - No elegible - Nivel {} - Nombre - Ganar trofeos - Control paterno - Actualizado - Tamaño - Versión - Título ID - Última vez que se utilizó - Tiempo utilizado - Nunca - - - - - Ha ocurrido un error. -Código del error: {} - Cancelar - Cerrar - No se ha podido cargar el archivo. - No se ha podido guardar el archivo. - Eliminar - Error - El archivo está dañado. - Activa el micrófono. - Espera un momento... - Seleccionar todo - - - - domingo - lunes - martes - miércoles - jueves - viernes - sábado - - - Enero - Febrero - Marzo - Abril - Mayo - Junio - Julio - Agosto - Septiembre - Octubre - Noviembre - Diciembre - - - enero - febrero - marzo - abril - mayo - junio - julio - agosto - septiembre - octubre - noviembre - diciembre - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 de - 2 de - 3 de - 4 de - 5 de - 6 de - 7 de - 8 de - 9 de - 10 de - 11 de - 12 de - 13 de - 14 de - 15 de - 16 de - 17 de - 18 de - 19 de - 20 de - 21 de - 22 de - 23 de - 24 de - 25 de - 26 de - 27 de - 28 de - 29 de - 30 de - 31 de - - Trofeo oculto - Hace 1 hora - Hace 1 minuto - Bronce - Oro - Platino - Plata - Hace {} horas - Hace {} minutos - - - - - Se eliminarán las aplicaciones seleccionadas y todos los datos relacionados, incluidos los datos guardados. - No hay elementos de contenido. - - - Se eliminarán los elementos de datos guardados seleccionados. - No hay datos guardados. - - Tema - Espacio libre - Borrar todo - - - - {} controles conectado - Núm - - - - - Preparándose para iniciar la aplicación... - - - - ¿Quieres cancelar la eliminación? - Eliminación completada. - ¿Quieres eliminar estos datos guardados? - - -
Detalles
- Actualizado -
- - ¿Quieres cancelar la carga? - No hay datos guardados. - Carga completada. - Cargando... - ¿Quieres cargar estos datos guardados? - - - ¿Quieres cancelar el guardado? - No se ha podido guardar el archivo. -No hay suficiente espacio libre en la tarjeta de memoria. Para guardar tu progreso en la aplicación, tienes que crear al menos {} de espacio libre. - -Para crear espacio libre, pulsa el botón PS para poner la aplicación en pausa y borrar otras aplicaciones o contenido. - No hay suficiente espacio libre en la tarjeta de memoria. -Para seguir usando la aplicación, tienes que crear al menos {} de espacio libre. - -Pulsa el botón PS para poner la aplicación en pausa y borrar otras aplicaciones o contenido. - Nuevos datos guardados - Guardado. - ¿Quieres guardar los datos? - Guardando... - Guardando... -No apagues el sistema ni cierres la aplicación. - ¿Quieres sobrescribir estos datos guardados? - -
-
- - - La siguiente aplicación se cerrará. - Datos para borrar: - - - - La aplicación se ha agregado a la pantalla de inicio. - Eliminar todo - Se eliminarán las notificaciones. - No se ha podido instalar. - Instalación completada. - Instalando... - No hay notificaciones. - ¡Has ganado un trofeo! - - - - Atrás - Completaste la configuración inicial. -¡Tu sistema Vita3K está listo! - Selecciona un idioma. - Vita3K requiere acceso completo a los archivos. -Toca 'Conceder Acceso' para continuar. - Conceder Acceso - Instalar Firmware. - Siguiente - - - - Iniciar - Continuar - - - - - Predeterminado - - - Nombre - Proveedor - Actualizado - Tamaño - Versión - - Este tema se eliminará. - - - Imagen - - - - - - - AAAA/MM/DD - DD/MM/AAAA - MM/DD/AAAA - - - Reloj de 12 horas - Reloj de 24 horas - - - - Idioma del sistema - - - - Danés - Alemán - Inglés (Reino Unido) - Inglés (Estados Unidos) - Español - Francés - Italiano - Holandés - Noruego - Polaco - Portugués (Brasil) - Portugués (Portugal) - Ruso - Finlandés - Sueco - Turco - - - - - - - - - - - Dirección IP - Máscara de subred - - Guardar & Cerrar - - - - Navegador - Navegador de Internet - Trofeos - Colección de trofeos - Ajustes - Gestor de contenido - - - -
Detalles
- Ganado - Nombre - No hay trofeos. -Podrás conseguir trofeos si usas una aplicación compatible con la función de trofeos. - No ganado - Original - Ordenar - Trofeos - Grado - Progreso - Actualizado -
- - - Seleccionar usuario - Crear usuario - Se ha creado el siguiente usuario: - El nombre ya está en uso. - Modificar usuario - Eliminar usuario - Seleciona el usuario que quieres eliminar. - Se eliminará el siguiente usuario: - Si elimina al usuario, se eliminarán todos los datos, los trofeos de dicho usuasio. - Se eliminará el usuario. -¿Deseas continuar? - Usuario eliminado. - Elegir avatar - Usuario - Confirmar - - - - Está disponible una nueva versión de Vita3K. - Atrás - ¿Quieres cancelar la actualización? -Si cancelas el proceso, la próxima vez que actualices, Vita3K comenzará la descarga desde este punto. - Descargando... -Cuando la descarga se haya completado, Vita3K se reiniciará automáticamente e instalará la actualización. - No se ha podido completar la actualización. - {} minutos restantes - Siguiente - {} segundos restantes - ¿Quieres actualizar Vita3K? - Ya está instalada una versión posterior de Vita3K. - Ya está instalada la versión más reciente de Vita3K. - Nuevas funciones de la versión {} - Actualizar - Versión {} - - - - Emulador de PlayStation Vita Vita3K - Vita3K es un emulador de PlayStation Vita de código abierto, escrito en C++ para Windows, Linux, macOS y Android. - El emulador todavía está en etapas tempranas del desarrollo, así que cualquier retroalimentación y pruebas serán sumamente apreciadas. - Descargar Firmware - Una guía completa sobre cómo configurar Vita3K está disponible en la página de - Inicio rápido - - Consulta la página de compatibilidad de juegos comerciales y Homebrew para ver qué juegos funcionan actualmente. - Lista de compatibilidad comercial - Lista de compatibilidad de Homebrew - ¡Las contribuciones son bienvenidas! - Más ayuda disponible en el canal #help del servidor de - Vita3K no tolera la piratería. Debes usar tus propios juegos. - Mostrar la próxima vez - -
diff --git a/lang/system/fi.xml b/lang/system/fi.xml deleted file mode 100644 index 8476671e5..000000000 --- a/lang/system/fi.xml +++ /dev/null @@ -1,402 +0,0 @@ - - - - - - Open Pref Path - Asenna laiteohjelmisto - Asenna .pkg - Asenna .zip, .vpk - Asenna lisenssi - Poistu - - - Last Apps used - - - Käyttäjien hallinta - - - Keyboard Controls - - - Tervetuloa - - - - - - Käyttöoppaan - Päivitys - - - Tämä sovellus ja kaikki siihen liittyvät tiedot, mukaan lukien tallennetut tiedot, poistetaan. - - Päivityshistoria - Versio {} - - Kelpo - Epäkelpo - Taso {} - Nimi - Trophyjen ansaitseminen - Lapsilukko - Päivitetty - Koko - Versio - - - - - Virhe on tapahtunut. -Virhekoodi: {} - Peruuta - Sulkea - Tiedostoa ei voitu ladata. - Tiedostoa ei voitu tallentaa. - Poista - Virhe - Tiedosto on vioittunt. - Ota mikrofoni käyttöön. - Ei - Odota hetki... - Valitse kaikki - - Kyllä - - sunnuntai - maanantai - tiistai - keskiviikko - torstai - perjantai - lauantai - - - Tammikuu - Helmikuu - Maaliskuu - Huhtikuu - Saattaa - Kesäkuuta - Heinäkuu - Elokuu - Syyskuu - Lokakuu - Marraskuu - Joulukuu - - - tammikuu - helmikuu - maaliskuu - huhtikuu - saattaa - kesäkuuta - heinäkuu - elokuu - syyskuu - lokakuu - marraskuu - joulukuu - - - - 1. - 2. - 3. - 4. - 5. - 6. - 7. - 8. - 9. - 10. - 11. - 12. - 13. - 14. - 15. - 16. - 17. - 18. - 19. - 20. - 21. - 22. - 23. - 24. - 25. - 26. - 27. - 28. - 29. - 30. - 31. - - - - 1. - 2. - 3. - 4. - 5. - 6. - 7. - 8. - 9. - 10. - 11. - 12. - 13. - 14. - 15. - 16. - 17. - 18. - 19. - 20. - 21. - 22. - 23. - 24. - 25. - 26. - 27. - 28. - 29. - 30. - 31. - - Piilotettu trophy - 1 tunti sitten - 1 minuutti sitten - Pronssi - Gold - Platina - Hopea - {} tuntia sitten - {} minuuttia sitten - - - - - Valitut sovellukset ja kaikki siihin liittyvät tiedot, mukaan lukien tallennetut tiedot, poistetaan. - Sisältökohteita ei ole. - - - Valitut tallennettua tietokohteet poistetaan. - Tallennettua tietoa ei ole saatavilla. - - Teema - Vapaa tila - Tyhjennä kaikki - - - - Num - - - - - Valmistellaan sovelluksen käynnistämistä... - - - - Haluatko peruuttaa poistamisen? - Poistaminen suoritettu. - Haluatko poistaa nämä tallennetut tiedot? - - -
Lisätiedot
- Päivitetty -
- - Haluatko peruuttaa latauksen? - Tallennettua tietoa ei ole saatavilla. - Lataus suoritettu. - Ladataan... - Haluatko ladata tallennetun tiedon? - - - Haluatko peruuttaa tallennuksen? - Tiedostoa ei voitu tallentaa. -Muistikortilla ei ole tarpeeksi vapaata tilaa. Jotta voit tallentaa sovelluksen käytön, sinun on vapautettava vähintään {} tallennustilaa. - -Voit vapauttaa tallennustilaa painamalla PS-näppäintä, jolloin sovellus siirtyy tauolle, ja poista sitten muut sovellukset tai muu sisältö. - Muistikortilla ei ole tarpeeksi vapaata tilaa. -Jotta voit jatkaa sovelluksen käyttämistä, sinun on vapautettava vähintään {} tallennustilaa. - -Aseta sovellus tauolle painamalla PS-näppäintä, ja poista sitten muut sovellukset tai muu sisältö. - Uusi tallennettu tieto - Tallennus suoritettu. - Haluatko tallentaa tiedot? - Tallennetaan... - Tallennetaan... -Älä katkaise virtaa järjestelmästä tai sulje sovellusta. - Haluatko kirjoittaa tallennetun tiedon päälle? - -
-
- - - Seuraava sovellus suljetaan. - Poistettavat tiedot: - - - - Sovellus on lisätty aloitusnäyttöön. - Poista kaikki - Ilmoitukset poistetaan. - Ei voitu asentaa. - Asennus suoritettu. - Asennetaan... - Ei ilmoituksia. - Olet ansainnut trophyn! - - - - Takaisin - Olet nyt saanut valmiiksi alkumäärityksen. -Vita3K-järjestelmäsi on nyt käyttövalmis! - Valitse kieli. - Vita3K vaatii täyden tiedostokäyttöoikeuden. -Napauta 'Myönnä Käyttöoikeus' jatkaaksesi. - Myönnä Käyttöoikeus - Asenna laiteohjelmisto. - Seuraava - - - - Rozpocznij - Jatka - - - - - Oletus - - - Nimi - Prlveluntarjoaja - Päivitetty - Koko - Versio - - Tämä teema poistetaan. - - - Kuva - - - - - - - VVVV/KK/PP - PP/KK/VVVV - KK/PP/VVVV - - - 12 tunnin kello - 24 tunnin kello - - - - Järjestelmän kieli - - - - Tanska - Saksa - Englanti (Yhdistynyt kuningaskunta) - Englanti (Yhdysvallat) - Espanja - Ranska - Italia - Hollanti - Norja - Puola - Portugali (Brasilia) - Portugali (Portugali) - Venäjä - Suomi - Ruotsi - Turkki - - - - - - - - - - - IP-osoite - Aliverkon peite - - Tallenna & Sulkea - - - - Selain - Internet-selain - Trophyt - Trophy-kokoelma - Asetukset - Sisällönhallinta - - - -
Lisätiedot
- Ansaittu - Nimi - Ei trophyjä. -Voit ansaita trophyjä käyttämällä trophy-ominaisuutta tukevaa sovellusta. - Ei ansaittu - Alkuperäinen - Lajittele - Trophyt - Luokka - Edistyminen - Päivitetty -
- - - Valitse käyttäjä - Luo käyttäjä - Seuraava käyttäjä on luotu: - Poista käyttäjä - Valitse käyttäjä, jonka haulat poistaa. - Seuraava käyttäjä poistetaan: - Jos poistat käyttäjän, hämen tallennetut tietonsa, trophyt poistetaan. - Käyttäjä poistetaan. -Oletko varma, että haluat jatkaa? - Käyttäjä on poistettu. - Valitse avatar - Kayttaja - Vahvista - - - - Vita3K on saatavissa uusi versio. - Takaisin - Haluatko peruuttaa päivityksen? -Jos peruutat, seuraavalla päivityskerralla Vita3K aloittaa lataamisen tästä kohdasta. - Ladataan... -Kun lataus on valmis, Vita3K käynnistyy automaattisesti uudelleen ja asentaa sitten uuden päivityksen. - Päivityksen suoritus ei onnistunut. - {} minuuttia jäljellä - Seuraava - {} sekuntia jäljellä - Haluatko päivittää Vita3K? - Vita3K viimeisin versio on jo asennettu. - Vita3K viimeisin versio on jo asennettu. - Uutta versiossa {} - Authors - Comments - Päivitä - Versio {} - -
diff --git a/lang/system/fr.xml b/lang/system/fr.xml deleted file mode 100644 index 43c91aeb4..000000000 --- a/lang/system/fr.xml +++ /dev/null @@ -1,517 +0,0 @@ - - - - - - Ouvrir le Dossier Pref - Installer le Firmware - Installer un .pkg - Installer un .zip, .vpk - Installer une Licence - Quitter - - - Dernières Apps utilisées - - - Gestion des Utilisateurs - - - Commandes du Clavier - - - Bienvenue - - - - - Vita3K: un émulateur PS Vita/PS TV. Le premier émulateur PS Vita/PS TV fonctionnel au monde. - Vita3K est un émulateur expérimental open-source de PlayStation Vita/PlayStation TV écrit en C++ pour les systèmes d'exploitation Windows, Linux, macOS et Android. - Crédit spécial: L'icône Vita3K a été désigné par: - Si vous souhaitez apporter votre contribution, consultez notre: - Visitez notre site web pour plus d'informations: - Si vous souhaitez nous soutenir, vous pouvez nous faire un don ou vous abonnez a notre: - L'équipe de Vita3K - Développeurs - Contributeurs - Supporters - - - - - Vérifier l'état de l'App - Copier le résumé de Vita3K - Ouvrir le rapport d'état - Créer le rapport d'état - Mettre à jour la base de données - - Copier l'info de l'App - Nom et ID du Titre - Résumé de l'App - - Créer un raccourci - - Créer - Editer - Supprimer - - - Ouvrir le dossier - DLCs - License - Cache des shaders - Journal des shaders - - - Manuel - Mise à jour - - - Cette application et toutes les données associées, y compris les données sauvegardées, vont être supprimées. - - Historique de mise à jour - Version {} - - Autorisé - Non autorisé - Niveau {} - Nom - Obtention de trophées - Contrôle parental - Mis à jour - Taille - Version - ID du Titre - Catégorie - Dernière fois utilisé - Temps utilisé - Jamais - - - - - Une erreur est survenue. -Code d'erreur: {} - Annuler - Fermer - Le chargement du fichier a échoué. - La sauvegarde du fichier a échoué. - Supprimer - Erreur - Le fichier est corrompu. - Activez le microphone. - Non - Merci de patienter... - Recherche - Sélectionner tout - - Oui - - dimanche - lundi - mardi - mercredi - jeudi - vendredi - samedi - - - Janvier - Février - Mars - Avril - Mai - Juin - Juillet - Août - Septembre - Octobre - Novembre - Décembre - - - janvier - février - mars - avril - mai - juin - juillet - août - septembre - octobre - novembre - décembre - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Trophée masqué - Il y a 1 heure - Il y a 1 minute - Bronze - Or - Platine - Argent - Il y a {} heures - Il y a {} minutes - - - - - Inconnue - Rien - Amorçable - Intro - Menu - En jeu - - En jeu + - Jouable - - - - - - Les applications sélectionnées et toutes les données associées, y compris les données sauvegardées, vont être supprimées. - Aucun élément de contenu. - - - Les données sauvegardées sélectionnées vont être supprimées. - Aucune données sauvegardées. - - Thème - Espace libre - Décocher tout - - - - {} contrôleurs connectés - Num - Aucun contrôleur compatible n'est connecté. -Connecter un contrôleur compatible avec SDL3. - - - - - Préparation au lancement de l'application... - - - - Annuler la suppression ? - Suppression terminée. - Supprimer ces données sauvegardées ? - - -
Détails
- Mis à jour -
- - Annuler le chargement ? - Aucune donnée sauvegardée. - Chargement terminé. - Chargement en cours... - Charger ces données sauvegardées ? - - - Annuler la sauvegarde ? - La sauvegarde du fichier a échoué. -Espace libre insuffisant sur la carte mémoire. Pour sauvegarder votre progression dans cette application, vous devez libérer au moins {} d'espace. - -Pour libérer de l'espace, appuyez sur la touche PS pour mette cette application en pause, puis supprimez d'autres applications ou contenus. - Espace libre insuffisant sur la carte mémoire. -Pour continuer à utiliser cette application,vous devez libérer au moins {} d'espace. - -Appuyez sur la touche PS pour mette cette application en pause, puis supprimez d'autres applications ou contenus. - Nouvelles données sauvegardées - Sauvegarde terminée. - Sauvegarder les données ? - Sauvegarde en cours... - Sauvegarde en cours... -Ne pas éteindre le système, ni fermer l'application. - Écraser ces données sauvegardées ? - -
-
- - - L'application suivante va être fermée. - Données à supprimer : - - - - Filtre - Trier les applications par - Toutes - Par région - USA - Europe - Japon - Asie - Par type - Commercial - Homebrew - Par état de compatibilité - Ver - Cat - Comp - Dernière fois - Rafraîchir - - - - L'application a été ajoutée à l'écran d'accueil. - Supprimer tout - Les notifications vont être supprimées. - L'installation a échoué. - Installation terminée. - Installation en cours... - Aucune notification. - Vous avez obtenu un trophée ! - - - - Précédent - Vous avez terminé l'installation initiale. -Votre système Vita3K est prêt ! - Sélectionner une langue. - Vita3K nécessite un accès complet aux fichiers. -Appuyez sur 'Autoriser l'accès' pour continuer. - Autoriser l'accès - Installer le Firmware. - Suivant - - - - Démarrer - Continuer - - Using configuration set for keyboard in control setting - Firmware not detected. Installation is highly recommended - Firmware font not detected. Installing it is recommended for font text in Live Area - Live Area Help - Browse in app list - D-pad, Left Stick, Wheel in Up/Down or using Slider - Démarrer App - Cliquer sur Démarrer ou Appuyer sur Croix - Show/Hide Live Area during app run - Appuyer sur PS - Exit Live Area - Click on Esc or Press on Circle - Manual Help - Browse page - D-pad, Left Stick, Wheel in Up/Down or using Slider, Click on </> - Hide/Show button - Left Click or Press on Triangle - Exit Manual - Click on Esc or Press on PS - - - - - - Musique système - - - Par défaut - - Trouver un thème personnalisé pour PSVita - - Nom - Prestataire - Mis à jour - Taille - Version - Content ID - - Ce thème va être supprimé. - - - Image - Ajouter une image - - - Delete Background - Add Background - - - - - AAAA/MM/JJ - JJ/MM/AAAA - MM/JJ/AAAA - - - Affichage 12h - Affichage 24h - - - - Langue système - - - - Danois - Allemand - Anglais (Royaume-Uni) - Anglais (États-Unis) - Espagnol - Français - Italien - Néerlandais - Norvégien - Polonais - Portugais (Brésil) - Portugais (Portugal) - Russe - Finnois - Suédois - Turc - - - - - - - - - - - Adresse IP - Masque de sous-réseau - - Sauvegarder & Fermer - - - - Navigateur - Navigateur Internet - Trophées - Collection de trophées - Paramètres - Gestionnaire de contenu - - - - Supprimer le trophée - Les informations sur les trophées enregistrées pour cet utilisateur seront supprimées. -
Détails
- Obtenu - Nom - Aucun trophée. -Pour obtenir des trophées, utilisez une application compatible avec cette fonctionnalité. - Pas obtenu - Original - Trier - Trophées - Rang - Progression - Mis à jour -
- - - Sélectionnez un utilisateur - Créer un utilisateur - L'utilisateur suivant a été créé : - Modifier un utilisateur - Ouvrir le dossier de l'utilisateur - Ce nom est déjà utilisé. - Supprimer un utilisateur - Sélectionnez l'utilisateur que vous souhaitez supprimer. - L'utilisateur suivant va être supprimé. - Si vous supprimez cet utilisateur, toutes ses données, trophées seront supprimés. - L'utilisateur va être supprimé. -Voulez-vous vraiment continuer? - Utilisateur supprimé. - Choisir un avatar - Réinitialiser l'avatar - Utilisateur - Confirmer - Connexion automatique de l'utilisateur - - - - Une nouvelle version de Vita3K est disponible. - Précédent - Annuler la mise à jour ? -Si vous annulez, Vita3K reprendra le téléchargement à ce point lors de la prochaine mise à jour. - Téléchargement en cours... -Une fois le téléchargement terminé, Vita3K redémarrera automatiquement et installera la nouvelle mise à jour. - La mise à jour n'a pas pu être terminée. - {} minutes restantes - Suivant - {} secondes restantes - Mettre à jour Vita3K ? - Une version plus récente de Vita3K est déjà installée. - La dernière version de Vita3K est déjà installée. - Nouveautés de la version {} - Auteurs - Commentaires - Mettre à jour - Version {} - -
diff --git a/lang/system/it.xml b/lang/system/it.xml deleted file mode 100644 index ba138367f..000000000 --- a/lang/system/it.xml +++ /dev/null @@ -1,422 +0,0 @@ - - - - - - Apri la Cartella Pref - Installa il Firmware - Installa un .pkg - Installa un .zip, .vpk - Installa una licenza - Esci - - - Ultime app utilizzate - - - Gestione utenti - - - Controlli della Tastiera - - - Benvevuto - - - - - - - - - Manuale - Aggiornamento - - - Questa applicazione e tutti i dati correlati, inclusi i dati salvati, saranno eliminati. - - Cronologia degli aggiornamenti - Versione {} - - Idoneo - Non idoneo - Livello {} - Nome - Guadagno di trofei - Filtro contenuti - Aggiornato - Dimensione - Versione - Ultimo usato - Tempo utilizzato - Mai - - - - - Si è verificato un errore. -Codice di errore: {} - Annulla - Impossibile caricare il file. - Impossibile salvare il file. - Chiudersi - Elimina - Errore - Il file è danneggiato. - Attiva il microfono. - Attendi... - Seleziona tutto - - - - domenica - lunedi - martedi - mercoledi - giovedi - venerdi - sabato - - - Gennaio - Febbraio - Marzo - Aprile - Maggio - Giugno - Luglio - Agosto - Settembre - Ottobre - Novembre - Dicembre - - - gennaio - febbraio - marzo - aprile - maggio - giugno - luglio - agosto - settembre - ottobre - novembre - dicembre - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Trofeo nascosto - 1 ora fa - 1 minuto fa - Bronzo - Oro - Platino - Argento - {} ore fa - {} minuti fa - - - - - Unknown - Niente - Avviabile - Intro - Menu - In Gioco - - In Gioco + - Giocabile - - - - - - Le applicazioni selezionate e tutti i dati correlati, inclusi i dati salvati, saranno eliminati. - Nessun elemento di contenuto. - - - I dati salvati selezionati saranno eliminati. - Non ci sono dati salvati. - - Tema - Spazio libero - Cancella tutto - - - - Num - - - - - Preparazione per l'avvio dell'applicazione... - - - - Vuoi annullare l'eliminazione? - Eliminazione completata. - Vuoi eliminare questi dati salvati? - - -
Dettagli
- Aggiornato -
- - Vuoi annullare il caricamento? - Non ci sono dati salvati. - Caricamento completato. - Caricamento in corso... - Vuoi caricare questi dati salvati? - - - Vuoi annullare il salvataggio? - Impossibile salvare il file. -Spazio libero insufficiente sulla scheda di memoria. Per potere salvare l'avanzamento dell'applicazione, devono essere disponibili almeno {} di spazio libero. - -Per creare spazio libero, premi il tasto PS per mettere in pausa l'applicazione, quindi elimina altre applicazioni o del contenuto. - Spazio libero insufficiente sulla scheda di memoria. -Per continuare a usare l'applicazione, devono essere disponibili almeno {} di spazio libero. - -Premi il tasto PS per mettere in pausa l'applicazione, quindi elimina altre applicazioni o del contenuto. - Nuovi dati salvati - Salvataggio completato. - Vuoi salvare i dati? - Salvataggio in corso... - Salvataggio in corso... -Non spegnere il sistema né chiudere l'applicazione. - Vuoi sovrascrivere questi dati salvati? - -
-
- - - La seguente applicazione sarà chiusa. - Dati da eliminare: - - - - L'applicazione è stata aggiunta alla schermata principale. - Elimina tutto - Le notifiche saranno eliminate. - Impossibile installare. - Installazione completata. - Installazione in corso... - Non ci sono notifiche. - Hai guadagnato un trofeo! - - - - Indietro - Hai finito la configurazione iniziale. -Il tuo sistema Vita3K è pronto per l'uso! - Seleziona una lingua. - Vita3K richiede accesso completo ai file. -Tocca 'Concedi Accesso' per continuare. - Concedi Accesso - Installa il Firmware. - Seguente - - - - Avvia - Continua - - - - - Predefinita - - - Nome - Fornitore - Aggiornato - Dimensione - Versione - - Questo tema sarà eliminato. - - - Immagine - - - - - - - AAAA/MM/GG - GG/MM/AAAA - MM/GG/AAAA - - - 12 ore - 24 ore - - - - Lingua del sistema - - - - Danese - Tedesco - Inglese (Regno Unito) - Inglese (Stati Uniti) - Spagnolo - Francese - Italiano - Olandese - Norvegese - Polacco - Portoghese (Brasile) - Portoghese (Portogallo) - Russo - Finlandese - Svedese - Turco - - - - - - - - - - - Indirizzo IP - Maschera di sottorete - - Salva & Chiudersi - - - - Browser - Browser per Internet - Trofei - Collezione dei trofei - Impostazioni - Gestione contenuto - - - -
Dettagli
- Guadagnato - Nome - Non ci sono trofei. -Puoi guadagnare trofei usando un'applicazione che supporta questa funzione. - Non guadagnato - Originale - Ordina - Trofei - Grado - Avanzamento - Aggiornato -
- - - Seleziona utente - Crea utente - L'utente seguente è stato creato: - Modifica utente - Il nome utente è già in uso. - Elimina utente - Seleziona l'utente che vuoi eliminare. - Il seguente utenti verrà eliminato: - Se elimini l'utente, i dati salvati, le trofei dell'utente saranno eliminati. - L'utente serà eliminato. -Vuoi davvero continuare? - Utente eliminato. - Scegli avatar - Utente - Conferma - - - - È disponibile una nuova versione del Vita3K. - Indietro - Vuoi annullare l'aggiornamento? -Se lo annulli, al prossimo aggiornamento Vita3K ricomincerà il download da questo punto. - Download in corso... -Al termine del download, Vita3K si riavvierà automaticamente e quindi installerà il nuovo aggiornamento. - Impossibile completare l'aggiornamento. - Tempo rimanente: {} minuti - Seguente - Tempo rimanente: {} secondi - Vuoi aggiornare Vita3K? - È già installata una versione più recente del Vita3K. - La versione più recente del Vita3K è già installata. - Nuove funzioni disponibili nella versione {} - Authors - Comments - Aggiorna - Versione {} - -
diff --git a/lang/system/ja.xml b/lang/system/ja.xml deleted file mode 100644 index 2da61174a..000000000 --- a/lang/system/ja.xml +++ /dev/null @@ -1,809 +0,0 @@ - - - - - - 保存先を開く - テクスチャパスを開く - ファームウェアをインストール - pkgをインストール - zip,vpkをインストール - ライセンスをインストール - 終了 - - - 最近プレイしたソフト - - - ユーザー管理 - - - キーボード - - - Welcome - - - - - Vita3K: PS Vita/PS TVエミュレーター。 世界初の動作するPS Vita/PS TVエミュレーターです。 - Vita3Kは試験的なオープンソースのC++で書かれた Windows、Linux、macOSとAndroid向けのPlayStation Vita/PlayStation TVエミュレーターです。 - 特別クレジット:Vita3Kアイコンデザイナー: - プロジェクトへの貢献に興味がある方は、GitHubをご覧ください: - 詳しくはサイトをご覧ください: - 私たちを応援したい方は、寄付や購読をお願いします: - Vita3K スタッフ - 開発者 - 貢献者 - サポーター - - - - - アプリの状態を確認 - Vita3K概要をコピー - 状態レポートを開く - 状態レポートを作成 - データベースを更新 - - - 名前とタイトルID - - - 作成 - 編集 - 削除 - - - フォルダを開く - 追加データ - ライセンス - シェーダーキャッシュ - シェーダーログ - - - マニュアル - アップデート - - - このアプリケーションと、関連するすべてのデータ(セーブデータを含む)を削除します。 - アプリの削除にはアプリのサイズと -ご利用のハードウェアにより時間がかかる場合があります。 - この追加データを削除しますか? - このライセンスを削除しますか? - - アップデート履歴 - バージョン {} - - 権利あり - 権利なし - レベル {} - 名前 - トロフィー獲得 - ペアレンタルコントロール - 更新日 - サイズ - バージョン - タイトルID - カテゴリー - 最後に起動した日時 - プレイ時間 - なし - - - {}秒 - {}分{}秒 - {}時間{}分{}秒 - {}日{}時間{}分{}秒 - {}週間{}日{}時間{}分{}秒 - - - - - エラーが起きました。 -エラーコード:{} - キャンセル - 閉じる - ロードできませんでした。 - セーブできませんでした。 - 削除 - エラー - ファイルが壊れています。 - マイクを有効にしてください。 - いいえ - しばらくお待ちください... - 検索 - すべて選択 - - 確認 - はい - - 日曜日 - 月曜日 - 火曜日 - 水曜日 - 木曜日 - 金曜日 - 土曜日 - - - 1月 - 2月 - 3月 - 4月 - 5月 - 6月 - 7月 - 8月 - 9月 - 10月 - 11月 - 12月 - - - 1月 - 2月 - 3月 - 4月 - 5月 - 6月 - 7月 - 8月 - 9月 - 10月 - 11月 - 12月 - - - - 1日 - 2日 - 3日 - 4日 - 5日 - 6日 - 7日 - 8日 - 9日 - 10日 - 11日 - 12日 - 13日 - 14日 - 15日 - 16日 - 17日 - 18日 - 19日 - 20日 - 21日 - 22日 - 23日 - 24日 - 25日 - 26日 - 27日 - 28日 - 29日 - 30日 - 31日 - - - - 1日 - 2日 - 3日 - 4日 - 5日 - 6日 - 7日 - 8日 - 9日 - 10日 - 11日 - 12日 - 13日 - 14日 - 15日 - 16日 - 17日 - 18日 - 19日 - 20日 - 21日 - 22日 - 23日 - 24日 - 25日 - 26日 - 27日 - 28日 - 29日 - 30日 - 31日 - - 隠しトロフィー - 1時間前 - 1分前 - ブロンズ - ゴールド - プラチナ - シルバー - {}時間前 - {}分前 - - - - - 不明 - プレイ不可能 - 起動可能 - イントロ - メニュー - プレイ開始 - - プレイ開始 + - プレイ可能 - - - - - 現在の互換性データベースを取得できませんでした。ファイアウォールやインターネットアクセスを確認し、後でもう一度試してください。 - アプリケーションの互換性データベースのダウンロードに失敗しました。更新日時: {}。後でもう一度試してください。 - ダウンロードされたアプリケーションの互換性データベースの読み込みに失敗しました。更新日時: {}。 - 互換性データベースは {} から {} に正常に更新されました。 - -{} 件の新しいアプリケーションがリストに追加され、合計が {} 件になりました! - 互換性データベースは {} から {} に正常に更新されました。 - -{} 件のアプリケーションがリストされています! - {} に更新された互換性データベースが正常にダウンロードおよび読み込まれました。 - -{} 件のアプリケーションがリストされています! - - - - - 選択したアプリケーションと、関連するすべてのデータ(セーブデータを含む)を削除します。 - コンテンツがありません。 - - - 選択したセーブデータを削除します。 - セーブデータがありません。 - - テーマ - 空き容量 - すべて解除 - - - - {}コントローラー接続済み - 番号 - ボタン割り当て - LEDカラー - カスタムカラーを使用 - コントローラのLEDにカスタムカラーを使用するには、このボックスをチェックします。 - - - - 利用可能なコントローラーが接続されていません。 -SDL3に対応したコントローラーを接続してください。 - ボタン割り当てをリセット - - - - - ボタン割り当て - 左スティック 上 - 左スティック 下 - 左スティック 右 - 左スティック 左 - 右スティック 上 - 右スティック 下 - 右スティック 右 - 右スティック 左 - 方向キー 上 - 方向キー 下 - 方向キー 右 - 方向キー 左 - 四角 ボタン - バツ ボタン - 丸 ボタン - 三角 ボタン - Start ボタン - Select ボタン - PS ボタン - L1 ボタン - R1 ボタン - Vita TV 専用 - L2 ボタン - R2 ボタン - L3 ボタン - R3 ボタン - フルスクリーン - タッチ操作の切り替え - 画面タッチと背面タッチを切り替えます。 - GUI表示の切り替え - アプリの実行中に画面上部のGUIの表示の切り替えを行います。 - その他 - テクスチャ置換を切り替える - そのキーはすでに使用済みです -   - - - - アプリケーションを始める準備中です... - - - - 削除をキャンセルしますか? - 削除しました。 - このセーブデータを削除しますか? - - -
詳細
- 更新日 -
- - ロードをキャンセルしますか? - セーブデータがありません。 - ロードしました。 - ロード中です... - このセーブデータをロードしますか? - - - セーブをキャンセルしますか? - セーブできませんでした。 -メモリーカードの空き容量が不足しています。アプリケーションの進み具合をセーブするには、{}以上の空き容量を用意する必要があります。 - -空き容量を用意するには、PSボタンを押してこのアプリケーションを一時停止し、他のアプリケーションやコンテンツを削除してください。 - メモリーカードの空き容量が不足しています。 -アプリケーションを続けるには、{}以上の空き容量を用意する必要があります。 - -PSボタンを押してこのアプリケーションを一時停止し、他のアプリケーションやコンテンツを削除してください。 - 新しいセーブデータ - セーブしました。 - データをセーブしますか? - セーブ中です... - セーブ中です... -電源を切ったり、アプリケーションを終了したりしないでください。 - このセーブデータを上書きしますか? - -
-
- - - 以下のアプリケーションを終了します。 - 削除するデータ: - - - - 絞り込み - 並び替え - すべて - リージョン - アメリカ - ヨーロッパ - 日本 - アジア - 種類 - 商用ゲーム - 自作ゲーム - 互換性状態別 - Ver - カテゴリー - 互換性 - 最終プレイ日時 - 更新 - - - - ホーム画面にアプリケーションが追加されました。 - すべて削除 - お知らせを削除します。 - インストールできませんでした。 - インストールしました。 - インストール中です... - お知らせはありません。 - トロフィーを獲得しました! - - - - 戻る - 初めの設定が完了しました。 -Vita3Kをお楽しみください。 - 言語を選んでください。 - ファームウェアをインストール。 - インストールされました: - フォントパッケージをダウンロード - ファームウェアファイルをインストール - 完了しました。 - 次へ - - - - - インストール中です、お待ちください... - ファームウェアのインストールに成功しました。 - ファームウェアバージョン: - ファームウェアフォントパッケージがありません,ダウンロードしてインストールしてください。 - ファームウェアフォントパッケージは、一部のアプリケーションや、 -GUIでのアジア地域のフォントサポートに必須です。 -また、一般的にGUIを正しく表示するために推奨されています。 - インストールしたファームウェアファイルを削除しますか? - - - ライセンスを選択 - work.bin/rifを選択 - zRIFを入力 - zRIF keyを入力 - zRIFをここに入力してください - Ctrl + C でコピー、Ctrl + V でペーストします。 - 先ほどのpkgを削除しますか? - 先ほどのwork.bin/rifを削除しますか? - - - インストール方法を選択 - ファイルを選択 - ディレクトリを選択 - {}互換性のあるコンテンツを持つアーカイブが見つかりました。 - {}アーカイブのコンテンツのインストールに成功しました: - アプリを更新: - {}コンテンツのインストールに失敗しました: - {}アーカイブに互換性のあるコンテンツを見つけられませんでした: - アーカイブを削除しますか? - - - ライセンスのインストールに成功しました。 - - - このコンテンツを再インストールしますか? - このコンテンツはすでにインストールされています。 - 現在あるデータを上書きし、再インストールしますか? - - - - - はじめる - つづける - - コントロール設定でキーを変更できます - ファームウェアが検出されませんでした。インストールを強くお勧めします - フォントのファームウェアが検出されませんでした。Liveareaのフォントのために、インストールを推奨します - アプリリストの閲覧 - 方向キー、左ステック、ホイールの上下または -スライダー - アプリの開始 - はじめるを押すか、バツボタンを押す - アプリの起動中にLiveareaの表示/非表示を切り替える - PSボタンを押す - Liveareaを閉じる - Escを押すか丸ボタンを押す - ページの閲覧 - 方向キー、左ステック、ホイールの上下またはスライダーまたは</> - ボタンの表示/非表示 - 左クリックまたは三角ボタンを押す - マニュアルを閉じる - Escを押すかPSボタンを押す - - - - - - デフォルト - - PSVita用カスタムテーマを探す - - 名前 - 提供者 - 更新日 - サイズ - バージョン - コンテンツID - - このテーマを削除します。 - - - 画像 - 画像を追加 - - - 背景を削除 - 背景を追加 - - - - - 年/月/日 - 日/月/年 - 月/日/年 - - - 12時間 - 24時間 - - - - システム言語 - - - - デンマーク語 - ドイツ語 - 英語 (イギリス) - 英語 (アメリカ) - スペイン語 - フランス語 - イタリア語 - オランダ語 - ノルウェー語 - ポーランド語 - ポルトガル語 (ブラジル) - ポルトガル語 (ポルトガル) - ロシア語 - フィンランド語 - スウェーデン語 - トルコ語 - - - - - - - - - モジュールモード - モジュールリスト - 希望するモジュールを選択してください。 - モジュールを検索 - リストをクリア - モジュールがありません。最新のPS Vitaファームウェアをダウンロードしてインストールしてください。 - リストを更新 - - - CPUバックエンド - 好みのCPUバックエンドを選択してください。 - 最適化を有効にする - 追加のCPU JIT最適化を有効にするには、チェックボックスをオンにします。 - - - リセット - バックエンド レンダラー - お好みのレンダーバックエンドを選択してください。 - GPU(再起動して適用) - Vita3Kを実行するGPUを選択してください。 - レンダラーの精度 - V-Sync - V-Syncを無効にすると、一部のゲームで速度の問題が解消されることがあります。 -視覚的なティアリングを避けるために有効にしておくことをお勧めします。 - サーフェス同期の無効化 - スピードハック。CPUとGPU間のサーフェス同期を無効にするには、チェックボックスをオンにします。 -サーフェス同期は一部のゲームで必要な場合があります。 -無効にすると大幅なパフォーマンス向上が期待できます(特にアップスケーリングがオンの場合)。 - スクリーンフィルタ - 適用するポストプロセッシングフィルタを設定します。 - 内部解像度のアップスケーリング - Vita3Kのアップスケーリングを有効にします。 -実験的:1倍を超える解像度で正しくレンダリングされることは保証されていません。 - 異方性フィルタリング - 異方性フィルタリングは、プレイヤーに対して傾斜した表面の画質を向上させる技術です。 -デメリットはありませんが、パフォーマンスに影響を与える可能性があります。 - テクスチャの置換 - テクスチャのエクスポート - テクスチャのインポート - テクスチャのエクスポート形式 - シェーダー - シェーダーキャッシュの使用 - ゲームの起動時に事前にコンパイルするためにシェーダーキャッシュを有効にするには、チェックボックスをオンにします。 -この機能を無効にするにはチェックを外してください。 - Spir-Vシェーダーの使用(非推奨) - 生成されたSpir-Vシェーダーをドライバに直接渡します。 -一部の有益な拡張が無効になり、すべてのGPUがこれに対応しているわけではありません。 - シェーダーキャッシュとログのクリア - - - - 決定ボタンの割り当て -「決定」ボタンを選択してください。 - これは、アプリケーションのダイアログで「決定」ボタンとして使用されるボタンです。 -一部のアプリケーションはこれを使用せず、デフォルトの決定ボタンを使用します。 - - × - PS TVモード - PS TVエミュレートモードを有効にするには、チェックボックスをオンにします。 - Showモード - チェックボックスをオンにしてShowモードを有効にします。 - デモモード - チェックボックスをオンにしてデモモードを有効にします。 - - - アプリをフルスクリーンで起動 - ログレベル - お好みのログレベルを選択してください。 - ログのアーカイブ - チェックボックスをオンにしてログのアーカイブを有効にします。 - Discord Rich Presenceを有効にして、Discordで実行しているアプリケーションを表示します。 - テクスチャキャッシュ - チェックボックスをオフにしてテクスチャキャッシュを無効にします。 - シェーダのコンパイルを表示 - チェックボックスをオフにして、シェーダのコンパイルダイアログの表示を無効にします。 - タッチパッドカーソルの表示 - チェックボックスをオフにして画面上にタッチパッドカーソルを表示しないようにします。 - 互換性の警告をログに表示 - GitHubの互換性の警告を有効にするには、チェックボックスをオンにします。 - アップデートをチェック - 起動時に自動的にアップデートを確認します。 - パフォーマンスオーバーレイ - 画面にパフォーマンス情報をオーバーレイとして表示します。 - 詳細 - お好みのパフォーマンス オーバーレイの詳細度を選択してください。 - 位置 - お好みのパフォーマンス オーバーレイの位置を選択してください。 - 大文字小文字を区別しないパス検索を有効にするにはチェックします。 -再起動時にリセットされます - Windows以外のプラットフォームでファイルを検索する際に、大文字小文字を区別せずに検索を試みることを許可します。 - エミュレートされたシステムストレージフォルダ - 現在のエミュレータのパス: - エミュレータのパスを変更 - Vita3Kエミュレータフォルダのパスを変更します。 -古いフォルダを新しい場所に手動で移動する必要があります。 - エミュレータのパスをリセット - Vita3Kエミュレータのパスをデフォルトにリセットします。 -古いフォルダを新しい場所に手動で移動する必要があります。 - カスタム設定 - カスタム設定をクリア - - - GUIの表示 - アプリケーションの起動後にGUIを表示するには、チェックボックスをオンにします。 - 情報バーの表示 - アプリケーションセレクタ内に情報バーを表示するには、チェックボックスをオンにします。 - 情報メッセージの表示 - 互換性情報メッセージを表示しないようにするには、チェックボックスをオフにします。 - システムアプリの表示 - ホーム画面でシステムアプリの表示を無効にするには、チェックボックスをオフにします。 -これらはメインメニューバーにのみ表示されます。 - Live Area画面を表示 - アプリケーションをクリックしたときにデフォルトでLive Areaを開くには、チェックボックスをオンにします。 -無効にすると、アプリケーションを右クリックして開きます。 - ディスプレイエリアの拡大 - 画面サイズに合わせてディスプレイエリアを拡大するには、チェックボックスをオンにします。 - グリッドモード - アプリリストをグリッドモードに設定するには、チェックボックスをオンにします。 - アプリアイコンのサイズ - お好みのアイコンサイズを選択してください。 - フォントサポート - アジアリージョン - 中国語と韓国語のフォントサポートを有効にするには、このボックスにチェックを入れます。 -これを有効にすると、より多くのメモリが必要で、エミュレータを再起動する必要があります。 - 一部のアプリケーションおよびGUIのアジアリージョンフォントサポートにはファームウェアフォントパッケージが必要です。 -通常はGUIでも推奨されています。 - テーマと背景 - 現在のテーマコンテンツID: - デフォルトテーマをリセット - テーマの背景を使用 - ユーザーの背景をクリア - 現在の開始背景: - 開始背景をリセット - 背景の透明度 - お好みの背景の透明度を選択してください。 -最小は不透明で、最大は透明です。 - 背景の遅延 - 背景を変更する前の遅延(秒)を選択してください。 - 開始画面に戻る遅延時間 - スタート画面に戻るまでの遅延時間(秒)を選択します。 - - - PSNステータス - PSネットワークの状態を選択してください。 - IPアドレス - サブネットマスク - HTTPの有効化 - このボックスにチェックを入れて、ゲームがインターネットでHTTPプロトコルを使用できるようにします。 - HTTPタイムアウト試行回数 - サーバーが応答しない場合の試行回数。 -インターネットが非常に不安定な場合や非常に遅い場合に有用です。 - HTTPタイムアウトのスリープ時間 - サーバーが応答しない場合のスリープ試行時間。 -インターネットが非常に不安定な場合や非常に遅い場合に有用です。 - HTTP読み取り終了回数 - データを読み取る際の試行回数。 -低い値はパフォーマンスを向上させる一方で、インターネットが非常に悪い場合のときはゲームを不安定にする可能性があります。 - HTTP読み取り終了までのスリープ時間 - データを読み取る際のスリープ試行時間。 -低い値はパフォーマンスを向上させる一方で、インターネットが非常に悪い場合はゲームを不安定にする可能性があります。 - - 保存して再起動 - 保存して適用 - 保存して閉じる - セーブをクリックして変更を保存します。 - - - - ブラウザー - インターネットブラウザー - トロフィー - トロフィーコレクション - 設定 - コンテンツ管理 - - - - トロフィーを削除 - このユーザーのトロフィー情報が削除されます。 -
詳細
- 獲得日 - 名前 - トロフィーがありません。 -トロフィー対応のアプリケーションで遊ぶとトロフィーを獲得できます。 - 未獲得 - オリジナル - 並べ替え - トロフィー数 - グレード - 達成率 - 更新日 -
- - - ユーザーを選ぶ - ユーザーを作成する - 以下のユーザーを作成しました。 - ユーザーを編集する - ユーザーフォルダを開く - その名前はすでに使われています。 - ユーザーを削除する - 削除するユーザーを選んでください。 - 以下のユーザーを削除します。 - ユーザーを削除すると、そのユーザーのセーブデータ、トロフィーは削除されます。 - ユーザーを削除します。 -本当によろしいですか? - ユーザーを削除しました。 - アバターを選ぶ - アバターをリセットする - 確定 - ユーザー自動ログインをする - - - - 新しいバージョンのVita3Kがあります。 - 戻る - アップデートをキャンセルしますか? -キャンセルした場合、Vita3Kは次回のアップデートでこの時点のダウンロードを再開します。 - ダウンロード中... -ダウンロードが完了すると、Vita3Kが自動的に再起動し、新しいアップデートがインストールされます。 - アップデートできませんでした。 - 残り {}分 - 次へ - 残り {}秒 - Vita3Kをアップデートしますか? - 新しいバージョンのVita3Kがインストールされています。 - 最新のバージョンのVita3Kがインストールされています。 - バージョン{}の新しい機能 - 著者 - コメント - アップデート - バージョン {} - - - - Vita3Kは、Windows、Linux、macOSとAndroid用にC++で書かれたオープンソースのPlayStation Vitaエミュレーターです。 - エミュレーターは未だ開発段階にあるため、フィードバックやテストは大歓迎です。 - ファームウェアをダウンロード - ファームウェアフォントパッケージをダウンロード - Vita3Kのセットアップ方法については、以下のページで解説しています。 - クイックスタート - ページ。 - 商用ゲーム互換性リストとHomebrew互換性リストで動作するものを御確認下さい。 - 商用ゲーム互換性リスト - Homebrew互換性リスト - 寄稿者を募集しています! - その他の相談は、#help チャンネルで行うことができます。 - Vita3Kは著作権侵害を認めません。所有ゲームの吸出しを行ってください。 - 次回以降も表示 - -
diff --git a/lang/system/ko.xml b/lang/system/ko.xml deleted file mode 100644 index d22496648..000000000 --- a/lang/system/ko.xml +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - 설치 경로 열기 - 펌웨어 설치 - .pkg 설치 - .zip, .vpk 설치 - 라이센스 설치 - 종료 - - - 마지막으로 사용한 앱 - - - 유저 관리 - - - 키보드 컨트롤 - - - 시작 - - - - - Vita3K: PS Vita/PS TV 에뮬레이터. 세계 최초의 작동하는 PS Vita/PS TV 에뮬레이터. - Vita3K는 C++로 작성된 윈도우, 리눅스, macOS와 안 드 로이드 OS용 실험적 오픈소스 PlayStation Vita/PlayStation TV 에뮬레이터입니다. - 기여에 관심이 있다면, 를 확인해 주세요: - 더 많은 정보를 확인하시려면 웹사이트를 방문해 주세요: - Vita3K 스태프 - 개발자 - 기여자 - 후원자 - - - - - 이름와 Title ID - - - 만들기 - 편집 - 삭제 - - - 폴더 열기 - 라이센스 - 셰이더 캐시 - 셰이더 로그 - - - 설명서 - 업데이트 - - - 이 애플리케이션 및 모든 관련 데이터(저장 데이터 포함)를 삭제합니다. - - 업데이트 이력 - 버전 {} - - 권리 있음 - 권리 없음 - 레벨 {} - 이름 - 트로피 획득 - 자녀보호 기능 - 갱신일 - 크기 - 버전 - - - {}초 - {}분 {}초 - {}시간 {}분 {}초 - {}일 {}시간 {}분 {}초 - {}주일 {}일 {}시간 {}분 {}초 - - - - - 에러기 발생했습니다. -에러 코드: {} - 취소 - 닫다 - 불러오지 못했습니다. - 저장하지 못했습니다. - 삭제 - 오류 - 파일이 손상되어 있습니다. - 마이크를 활성화해 주십시오. - 아니요 - 잠시 기다려 주십시오... - 모두 선택 - - - - 일요일 - 월요일 - 화요일 - 수요일 - 목요일 - 금요일 - 토요일 - - - 1월 - 2월 - 3월 - 4월 - 5월 - 6월 - 7월 - 8월 - 9월 - 10월 - 11월 - 12월 - - - 1월 - 2월 - 3월 - 4월 - 5월 - 6월 - 7월 - 8월 - 9월 - 10월 - 11월 - 12월 - - - - 1일 - 2일 - 3일 - 4일 - 5일 - 6일 - 7일 - 8일 - 9일 - 10일 - 11일 - 12일 - 13일 - 14일 - 15일 - 16일 - 17일 - 18일 - 19일 - 20일 - 21일 - 22일 - 23일 - 24일 - 25일 - 26일 - 27일 - 28일 - 29일 - 30일 - 31일 - - - - 1일 - 2일 - 3일 - 4일 - 5일 - 6일 - 7일 - 8일 - 9일 - 10일 - 11일 - 12일 - 13일 - 14일 - 15일 - 16일 - 17일 - 18일 - 19일 - 20일 - 21일 - 22일 - 23일 - 24일 - 25일 - 26일 - 27일 - 28일 - 29일 - 30일 - 31일 - - 숨겨진 트로피 - 1 시간 전 - 1 분 전 - 브론즈 - 골드 - 플래티넘 - 실버 - {} 시간 전 - {} 분 전 - - - - - - - - 선택한 애플리케이션 및 모든 관련 데이터(저장 데이터 포함)를 삭제합니다. - 콘텐츠가 없습니다. - - - 선택한 세이브 데이터를 삭제합니다. - 저장 데이터가 없습니다. - - 테마 - 빈 용량 - 모두 해제 - - - - 번호 - - - - - 애플리케이션을 시작하기 위한 준비중입니다... - - - - 삭제를 취소하시겠습니까? - 삭제했습니다. - 이 저장 데이터를 삭제하시겠습니까? - - -
상세
- 갱신일 -
- - 불러오기를 취소하시겠습니까? - 저장 데이터가 없습니다. - 불러오기를 완료했습니다. - 불러오기 중입니다... - 이 저장 데이터를 불러오시겠습니까? - - - 저장을 취소하시겠습니까? - 저장하지 못했습니다. -메모리 카드의 빈 용량이 부족합니다. 애플리케이션의 진행 상태를 저장하려면 {} 이상의 빈 용량이 필요합니다. - -빈 용량을 확보하시려면 PS 버튼을 눌러 이 애플리케이션을 일시 정지하신 후 다른 애플리케이션 및 콘텐츠를 삭제해 주십시오. - 메모리 카드의 빈 용량이 부족합니다. -애플리케이션을 계속하려면 {} 이상의 빈 용량이 필요합니다. - -PS 버튼을 눌러 이 애플리케이션을 일시 정지하신 후 다른 애플리케이션 및 콘텐츠를 삭제해 주십시오. - 새 저장 데이터 - 저장했습니다. - 데이터를 저장하시겠습니까? - 저장중입니다... - 저장중입니다… -전원을 끄거나 애플리케이션을 종료하지 마십시오. - 이 저장 데이터를 덮어쓰시겠습니까? - -
-
- - - 아래의 애플리케이션을 종료합니다. - 삭제된 데이터: - - - - 홈 화면에 애플리케이션이 추가되었습니다. - 모두 삭제 - 알림을 삭제합니다. - 설치하지 못했습니다. - 설치했습니다. - 설치중입니다... - 알림이 없습니다. - 홈 화면에 애플리케이션이 추가되었습니다. - - - - 돌아가기 - 초기 설정이 완료되었습니다. -Vita3K를 즐기러 GOGO! - 언어를 선택해 주십시오. - 펌웨어 설치. - 완료. - 다음 - - - - 시작 - 계속 - - - - - 오리지널 - - - 이름 - 제공자 - 갱신일 - 크기 - 버전 - - 이 테마를 삭제합니다. - - - 이미지 - - - - - - - 년/월/일 - 일/월/년 - 월/일/년 - - - 12 시간 - 24 시간 - - - - 시스템 언어 - - - - 덴마크어 - 독일어 - 영어 (영국) - 영어 (미국) - 스페인어 - 프랑스어 - 이탈리아어 - 네덜란드어 - 노르웨이어 - 폴란드어 - 포르투갈어 (브라질) - 포르투갈어 (포르투갈) - 러시아어 - 핀란드어 - 스웨덴어 - 터키어 - - - - - - - - - - - IP 어드레스 - 서브넷 마스크 - - 저장 & 닫다 - - - - 브라우저 - 인터넷 브라우저 - 트로피 - 트로피 컬렉션 - 설정 - 콘텐츠 관리 - - - -
상세
- 획득일 - 이름 - 트로피가 없습니다. -트로피에 대응하는 애플리케이션으로 즐기시면 트로피를 획득할 수 있습니다. - 미획득 - 오리지널 - 정렬 - 트로피 수 - 그레이드 - 달성률 - 갱신일 -
- - - 유저 선택 - 유저 작성하기 - 아래의 유저를 작성했습니다. - 유저 편집 - 이미 사용중인 이름입니다. - 유저 삭제하기 - 삭제할 유저를 선택해 주세요. - 아래의 유저를 삭제합니다. - 유저를 삭제하면 해당 유저의 저장 데이터,트로피 클립도 삭제됩니다. - 유저를 삭제합니다. -정말 계속할까요? - 유저를 삭제했습니다. - 아바타 선택하기 - 아바타 리셋 - 확인 - 유저를 자동 로그인 - - - - 새 버전의 Vita3K 있습니다. - 돌아가기 - 업데이트를 취소하시겠습니까? -취소한 경우 다음에 업데이트할 때는 이 지점부터 계속하여 다운로드합니다. - 다운로드 중입니다... -다운로드 후에 Vita3K 자동으로 재기동하여 새로운 업데이트를 설치합니다. - 업데이트하지 못했습니다. - 남은 시간 {} 분 - 다음 - 남은 시간 {} 초 - Vita3K를 업데이트하시겠습니까? - 새 버전의 Vita3K가 설치되어 있습니다. - 최신 버전의 Vita3K가 설치되어 있습니다. - 버전 {}의 새로운 기능 - 업데이트 - 버전 {} - -
diff --git a/lang/system/nl.xml b/lang/system/nl.xml deleted file mode 100644 index 765184adf..000000000 --- a/lang/system/nl.xml +++ /dev/null @@ -1,418 +0,0 @@ - - - - - - Open Opslag Pad - Installeer Firmware - Installeer .pkg - Installeer .zip, .vpk - Installeer een licentie - Afsluiten - - - Laatste gebruikte apps - - - Gebruikerbeheer - - - Toetsenbord Besturing - - - Welkom - - - - - - - - - Handleiding - Update - - - Deze applicatie en alle gerelateerde data, inclusief opgeslagen data, worden verwijderd. - - Updategeschiedenis - Versie {} - - Geschikt - Niet geschikt - Niveau {} - Naam - Trofeeën verdienen - Ouderlijk toezicht - Geüpdatet - Grootte - Versie - Laatst gebruikt - Gebruikte tijd - Nooit - - - - - Er is een fout opgetreden. -Foutcode: {} - Annuleren - Sluiten - Kan het bestand niet laden. - Kan het bestand niet opslaan. - Verwijderen - Fout - Het bestand is beschadigd. - Schakel de microfoon in. - Nee - Een ogenblik geduld... - Alles selecteren - - Ja - - zondag - maandag - dinsdag - woensdag - donderdag - vrijdag - zaterdag - - - Januari - Februari - Maart - April - Mei - Juni - Juli - Augustus - September - Oktober - November - December - - - januari - februari - maart - april - mei - juni - juli - augustus - september - oktober - november - december - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Verborgen trofee - 1 uur geleden - 1 minuut geleden - Brons - Goud - Platina - Zilver - {} uur geleden - {} minuten geleden - - - - - Unknown - Niente - Avviabile - Intro - Menu - In Gioco - - In Gioco + - Giocabile - - - - - - De geselecteerde applicaties en alle gerelateerde data, inclusief opgeslagen data, worden verwijderd. - Er zijn geen content-items. - - - De geselecteerde opgeslagen data-items worden verwijderd. - Er zijn geen opgeslagen data. - - Thema - Vrije ruimte - Alles wissen - - - - Num - - - - - Bezig met het voorbereiden voor het starten van de applicatie... - - - - Wil je het verwijderen annuleren? - Het verwijderen is voltooid. - Wil je deze opgeslagen data verwijderen? - - -
Details
- Geüpdatet -
- - Wil je het laden annuleren? - Er zijn geen opgeslagen data. - Het laden is voltooid. - Bezig met laden... - Wil je deze opgeslagen data laden? - - - Wil je het opslaan annuleren? - Kan het bestand niet opslaan. -Er is onvoldoende vrije ruimte op de geheugenkaart. Om je voortgang in de applicatie te kunnen opslaan, moet je tenminste {} vrije ruimte maken. - -Om vrije ruimte te maken, druk je op de PS-toets om deze applicatie te pauzeren en verwijder je vervolgens andere applicaties of content. - Er is onvoldoende vrije ruimte op de geheugenkaart. -Om de applicatie te kunnen blijven gebruiken, moet je tenminste {} vrije ruimte maken. - -Druk op de PS-toets om deze applicatie te pauzeren en verwijder andere applicaties of content. - Nieuwe opgeslagen data - Opslaan voltooid. - Wil je de data opslaan? - Bezig met opslaan... - Bezig met opslaan... -Schakel het systeem niet uit en sluit de applicatie niet. - Wil je deze opgeslagen data overschrijven? - -
-
- - - De volgende applicatie wordt gesloten. - Te verwijderen data: - - - - De applicatie is toegevoegd aan het beginscherm. - Alles verwijderen - De meldingen worden verwijderd. - Kan niet installeren. - De installatie is voltooid. - Bezig met installeren... - Er zijn geen meldingen. - Je hebt een trofee gewonnen! - - - - Terug - Je hebt de initiële setup nu voltooid. -Je Vita3K-systeem is gereed! - Selecteer een taal. - Installeer Firmware. - Volgende - - - - Starten - Doorgaan - - - - - Standaard - - - Naam - Provider - Geüpdatet - Grootte - Versie - - Dit thema wordt verwijderd. - - - Afbeelding - - - - - - - YYYY/MM/DD - DD/MM/YYYY - MM/DD/YYYY - - - 12-uursklok - 24-uursklok - - - - Systeemtaal - - - - Deens - Duits - Engels (Verenigd Koninkrijk) - Engels (Verenigde Staten) - Spaans - Frans - Italiaans - Nederlands - Noors - Pools - Portugees (Brazilië) - Portugees (Portugal) - Russisch - Fins - Zweeds - Turks - - - - - - - - - - - IP-adres - Subnetmasker - - Opslaan & Sluiten - - - - Browser - Internetbrowser - Trofeeën - Trofeecollectie - Instellingen - Content manager - - - -
Details
- Gewonnen - Naam - Er zijn geen trofeeën. -Je kunt trofeeën verdienen door een applicatie te gebruiken die de trofeefunctie ondersteunt. - Niet verdiend - Origineel - Sorteren - Trofeeën - Rang - Voortgang - Geüpdatet -
- - - Gebruiker selecteren - Gebruiker maken - De volgende gebruiker is gemaakt: - Gebruiker bewerken - De naam is al in gebruik. - Gebruiker verwijderen - Selecteer de gebruiker die u wil verwijderen. - De volgende gebruiker wordt verwijderd: - Als je de gebruiker verwijdert, worden de door de gebruiker opgeslagen data, trofeeën verwijderd. - De gebruiker wordt verwijderd. -Weet je zeker dat je doorgaan? - Gebruiker verwijderd. - Avatar kiezen - Gebruiker - Bevestigen - - - - Een nieuwe versie van Vita3K is beschikbaar. - Terug - Wil je de update annuleren? -Als je annuleert zet het Vita3K bij de volgende update het downloaden verder vanaf dit punt. - Bezig met downloaden... -Nadat het downloaden is voltooid, Vita3K automatisch opnieuw gestart en wordt de nieuwe update geïnstalleerd. - Kan de update niet voltooien. - {} minuten resterend - Volgende - {} seconden resterend - Wil je Vita3K updaten? - Een recentere versie van Vita3K is al geïnstalleerd. - De meest recente versie van Vita3K is al geïnstalleerd. - Nieuwe functies in versie {} - Update - Versie {} - -
diff --git a/lang/system/no.xml b/lang/system/no.xml deleted file mode 100644 index 32de1a877..000000000 --- a/lang/system/no.xml +++ /dev/null @@ -1,397 +0,0 @@ - - - - - - Open Pref Path - Install Firmware - Install .pkg - Install .zip, .vpk - Install License - Avslutte - - - Last Apps used - - - Brukeradminstrasjon - - - Keyboard Controls - - - Welcome - - - - - - Håndboken - Oppdater - - - Dette programmet og all relatert data, inkludert lagreda data, vil bli slettet. - - Oppdater historikk - Versjon {} - - Valgbar - Ikke valgbar - Nivå {} - Navn - Trophy-oppnåing - Foreldrakontroll - Oppdatert - Størrelse - Versjon - - - - - Det har oppstått en feil. -Feilkode: {} - Avbryt - Lukke - Kunne ikke lagre filen. - Kunne ikke lagre filen. - Slett - Feil - Filen er korrupt. - Aktiver mikrofonen. - Nei - Vennligst vent... - Velg alle - - Ja - - Søndag - Mandag - Tirsdag - onsdag - Torsdag - Fredag - lørdag - - - Januar - Februar - Mars - April - mai - juni - Juli - August - September - Oktober - November - December - - - Januar - Februar - Mars - April - mai - juni - Juli - August - September - Oktober - November - December - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Skjult trophy - 1 time siden - 1 minutt sidan - Bronse - Gull - Platina - Sølv - {} timer siden - {} minutter sidan - - - - - De valgte programmene og alle de tilknyttede dataene, inkludert lagreda data, vil bli slettet. - Det er ingen innholdselementer. - - - De valgte lagrede dataelementene vil bli slettet. - Det er ingen lagret data. - - Tema - Ledig plass - Slett alle - - - - Num - - - - - Forbereder oppstart av programmet... - - - - Vil du avbryte slettingen? - Sletting fullført. - Vil du slette disse lagrede dataene? - - -
Detaljer
- Oppdatert -
- - Vil du avbryte lastingen? - Det finnes ingen lagret data. - Lasting fullført. - Laster... - Vil du laste disse lagrede dataene? - - - Vil du avbryte lagringen? - Kunne ikke lagre filen. -Det er ikke nok ledig plass på minnekortet. For å lagre arbeidet ditt i programmet, må du frigjøre minimum {} ledig plass. - -For å opprette denne ledige plassen må du trykke på PS-knappen for å stoppe dette programmet, og deretter slette andre programmer eller innhold. - Det er ikke nok ledig plass på minnekortet. -For å fortsette å bruke programmet, må du frigjøre minst {} ledig plass. - -Trykk på PS-knappen for å stoppe dette programmet og deretter slette andre programmer eller innhold. - Nylagret data - Lagring fullført. - Vil du lagre dataene? - Lagrer... - Lagrer... -Slå ikke av systemet og lukk ikke programmet. - Vil du overskrive disse lagrede dataene? - -
-
- - - Følgende program vil lukkes. - Data som skal slettes: - - - - Programmet har blitt lagt til hjem-skjermen. - Slett alt - Varslingene vil bli slettet. - Kunne ikke installere. - Installering fullført. - Installerer... - Det er ingen varslinger. - Du har oppnådd et trophy! - - - - Tilbake - Du har nå fullført oppstartsinnstillingen. -Vita3K-systemet ditt er klart! - Velg et språk. - Neste - - - - Start - Fortsett - - - - - Standard - - - Navn - Leverandør - Oppdatert - Størrelse - Versjon - - Dette temaet vil bli slettet. - - - Bilde - - - - - - - ÅÅÅÅ/MM/DD - DD/MM/ÅÅÅÅ - MM/DD/ÅÅÅÅ - - - 12- timers klokke - 24- timers klokke - - - - Systemspråk - - - - Dansk - Tysk - Engelsk (Storbritannia) - Engelsk (USA) - Spansk - Fransk - Italiensk - Hollandsk - Norsk - Polsk - Portugisisk (Brasil) - Portugisisk (Portugal) - Russisk - Finsk - Svensk - Tyrkisk - - - - - - - - - - - IP-adresse - Subnet-maske - - Lagre & Lukke - - - - Nettleser - Nettleser - Trophies - Trophy-samling - Innstillinger - Innholdsadministrator - - - -
Detaljer
- Oppnådd - Navn - Det finnes ingen trophies. -Du kan Oppnå trophies ved å bruke et program som Støtter trophy-funksjonen. - Ikke oppnådd - Original - Sorter - Trophies - Karakter - Fremdrift - Opdaterest -
- - - Velg bruker - Opprett bruker - Følgende bruker er opprettet: - Navnet er allerede i bruk. - Slett bruker - Velg brukeren du vil slette. - Følgende bruker vil bli slettet: - Hvis du sletter brukeren, vil denne brukerens lagrede data, trofeer bli slettet. - Brukeren vil bli slettet. -Er du sikker på at du vil fortsette? - Bruker slettet. - Velg avatar - Bruker - Bekreft - - - - En ny versjon av Vita3K er tilgjengelig. - Tilbake - Vil du avbryte oppdateringen? -Hvis du avbryter, kommer Vita3K til å starte nedlastingen fra dette punktet neste gang du oppdaterer. - Laster ned... -Etter at Etter at nedlastingen er fullført, vil Vita3K starte på nytt automatisk og deretter installere den nye oppdateringen. - Kunne ikke fullføre oppdateringen. - {} minutter gjenstår - Neste - {} sekunder gjenstår - Vil du oppdatere Vita3K? - Den siste versjonen av Vita3K er allerede installert. - Den siste versjonen av Vita3K er allerede installert. - Nye funksjoner i versjon {} - Oppdater - Versjon {} - -
diff --git a/lang/system/pl.xml b/lang/system/pl.xml deleted file mode 100644 index 3ee0e9e54..000000000 --- a/lang/system/pl.xml +++ /dev/null @@ -1,948 +0,0 @@ - - - - - - Otwórz preferowaną ścieżkę emulatora - Otwórz ścieżkę folderu z teksturami - Zainstaluj oprogramowanie - Zainstaluj .pkg - Zainstaluj .zip, .vpk - Zainstaluj licencję - Zamknij - - - Ostatnio używane aplikacje - Pusto - - - Wątki - Semafory - Mutexy - Lekkie mutexy - Zmienne stanu - Lekkie zmienne stanu - Flagi zdarzeń - Alokacje pamięci - Demontaż - - - Zarządzanie użytkownikami - - - Klawiatura - - - Witamy - - - - - Vita3K: Emulator systemów PS Vita/PS TV. Pierwszy na świecie funkcjonalny emulator PS Vita/PS TV. - Vita3K jest otwarto-źródłowym emulatorem PlayStation Vita pisanym w języku C++ dla systemu Windows, Linux, macOS oraz Android. - Chonorowe wspomnienie, ikona dla Vita3K byłą zaprojektowana przez: - Jeśli interesuje cie pomoc przy projekcie, zerknij na: - Odwiedź naszą stronę internetową po więcej informacji: - Jeśli chcesz nas wesprzeć, możesz wysłać dotację lub zasubskrybować nasz profil: - Zespół 'Vita3K' - Deweloperzy - Współautorzy - Wspierający - - - - - Sprawdź stan aplikacji - Kopiuj podsumowanie Vita3K - Otwórz raport o stanie - Stwórz raport o stanie - Aktualizuj bazę danych - - - Nazwa i numer identyfikacyjny aplikacji - Podsumowanie aplikacji - - - Utwórz - Edytuj - Usuń - - - Otwórz folder - Dodatkowa zawartość (DLC) - Licencja - Pamięć podręczna cieni - Dziennik zdarzeń cieni - - - Instrukcja obsługi - Aktualizuj - - - Aplikacja i wszystkie powiązane dane, w tym zapisane dane, zostaną usunięte. - Usunięcie aplikacji może zająć trochę czasu, -jest to zależne od jej rozmiaru i twojego sprzętu. - Czy chcesz usunąć dane dodatkowej zawartości (DLC)? - Czy chcesz usunąć tę licencję? - - Historia aktualizacji - Wersja {} - - Spełnia warunki - Nie spełnia warunków - Poziom {} - Nazwa - Zdobywanie trofeów - Kontrola rodzicielska - Zaktualizowano - Rozmiar - Wersja - Identyfikator tytułu - Kategoria - Ostatnio uruchamiane - Uruchomione przez - Nigdy - - - {}s - {}m:{}s - {}h:{}m:{}s - {}d:{}h:{}m:{}s - {}w:{}d:{}h:{}m:{}s - - - - - Wystąpił błąd. -Kod błędu: {} - Anuluj - Zamknij - Nie można załadować pliku. - Nie można zapisać pliku. - Usuń - Błąd - Plik jest uszkodzony. - Włącz mikrofon. - Nie - OK - Czekaj... - Wyszukaj - Zaznacz wszystko - - Zatwierdź - Tak - - niedziela - poniedziałek - wtorek - środa - czwartek - piątek - sobota - - - Styczeń - Luty - Marzec - Kwiecień - Maj - Czerwiec - Lipiec - Sierpień - Wrzesień - Październik - Listopad - Grudzień - - - styczeń - luty - marzec - kwiecień - maj - czerwiec - lipiec - sierpień - wrzesień - październik - listopad - grudzień - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Ukryte trofeum - 1 godz. temu - 1 min temu - Brązowy - Złoty - Platynowy - Srebrny - {} godz. temu - {} min temu - - - - - Nieznany - Nie uruchamia się - Uruchamia się - Intro - Menu - W grze - - W grze + - Grywalne - - - - - Nie udało się znaleźć bazy kompatybilności, sprawdź zaporę sieciową/dostęp do internetu, spróbuj ponownie później. - Nie udało się pobrać bazy kompatybilności aplikacji zaktualizowanej {}, spróbuj ponownie później. - Nie udało się załadować bazy kompatybilności aplikacji zaktualizowanej {}. - Baza lista kompatybilności została zaktualizowana pomyślnie z {} na {}. - -{} nowych aplikacji dostępnych w bazie, zwiększając ją do {}! - Baza lista kompatybilności została zaktualizowana pomyślnie z {} na {}. - -{} aplikacji dostępnych w bazie! - Lista kompatybilności zaktualizowana o {} została pomyślnie pobrana i załadowana. - -{} aplikacji dostępnych w bazie! - - - - Kompilacja ceni - {} potoków cieni skompilowanych - {} cieni skompilowanych - - - - - Wybrane aplikacje i wszystkie powiązane dane, w tym zapisane dane, zostaną usunięte. - Brak elementów zawartości. - - - Zaznaczone elementy zapisanych danych zostaną usunięte. - Brak zapisanych danych. - - Motyw - Wolne miejsce - Wyczyść wszystko - - - - {} kontrolerów podłączonych - Num - Wsparcie dla ruchu - Przypisz przyciski ponownie - Kolor LED - Użyj własnego koloru - Zaznacz, aby używać własnego koloru dla światła LED obecnego w kontrollerze. - Czerwony - Zielony - Niebieski - Niepodłączono żadnego ze wspieranych kontrolerów. -Proszę, podłącz kontroler, który jest kompatybilny z SDL3. - Wyłącz wsparcie dla ruchu - Restuj przypisanie przycisków kontrollera - - - - - Przypisany przycisk - Lewy drążek góra - Lewy drążek dół - Lewy drążek prawo - Lewy drążek lewo - Prawy drążek góra - Prawy drążek dół - Prawy drążek prawo - Prawy drążek lewo - Krzyżak (D-Pad) góra - Krzyżak (D-Pad) dół - Krzyżak (D-Pad) prawy - Krzyżak (D-Pad) lewy - Przycisk 'Kwadrat' - Przycisk 'Krzyżyk' - Przycisk 'Kółko' - Przycisk 'Trójkąt' - Przycisk 'Start' - Przycisk 'Select' - Przycisk 'PS' - Przycisk 'L1' - Przycisk 'R1' - Tylko w trybie PS TV. - Przycisk 'L2' - Przycisk 'R2' - Przycisk 'L3' - Przycisk 'R3' - Interfejs graficzny - Pełny ekran - Włącz obsługę dotyku - Pozwala na korzystanie z ekranu dotykowego na ekranie i z tyłu konsoli - Włącz widoczność graficznego interfejsu użytkownika - Pozwala na pokazywanie i ukrywanie interfejsu emulatora u góry ekranu podczas gdy aplikacja jest uruchomiona. - Rożne - Aktywuj zamianę tekstur - Zrób zrzut ekranu - Klawisz/przycisk jest już przypisany lub zarezerwowany. - - - - - Przygotowanie do uruchomienia aplikacji... - - - - Czy chcesz anulować usunięcie? - Usuwanie ukończone. - Czy chcesz usunąć te zapisane dane? - - -
Szczegóły
- Zaktualizowano -
- - Czy chcesz anulować ładowanie? - Brak zapisanych danych. - Ładowanie ukończone. - Ładowanie... - Czy chcesz załadować zapisane dane? - - - Czy chcesz anulować zapis? - Nie można zapisać pliku. -Na karcie pamięci nie ma wystarczającej ilości wolnego miejsca. Na karcie pamięci nie ma wystarczającej ilości wolnego miejsca. Aby zapisać postęp w aplikacji, należy uzyskać co najmniej {} wolnego miejsca. - -Aby uzyskać wolne miejsce, naciśnij przycisk PS w celu wstrzymania działania tej aplikacji, a następnie usuń inne aplikacje lub zawartość. - Na karcie pamięci nie ma wystarczającej ilości wolnego miejsca. -Aby kontynuować korzystanie z tej aplikacji, należy uzyskać co najmniej {} wolnego miejsca. - -Naciśnij przycisk PS, aby wstrzymać działanie tej aplikacji, a następnie usuń inne aplikacje lub zawartość. - Nowe zapisane dane - Zapis ukończony. - Czy chcesz zapisać te dane? - Zapisywanie... - Zapisywanie... -Nie wyłączaj systemu ani nie zamykaj aplikacji. - Czy chcesz zastąpić zapisane dane? - -
-
- - - Następująca aplikacja zostanie zamknięta. - Dane do usunięcia: - - - - Filtruj - Sortuj aplikacje według - Wszystkie - Wg regionu - Stany Zjednoczone - Europa - Japonia - Azja - Wg typu - Aplikacje komercyjne - Homebrew - Wg stopnia kompatybilności - Wersja - Kategoria - Kompatybilność - Ostatnio uruchamiane - Odśwież - - - - Aplikacja została dodana do ekranu głównego. - Usuń wszystko - Powiadomienia zostaną usunięte. - Nie można zainstalować pliku. - Instalacja ukończona. - Instalowanie... - Brak powiadomień. - Udało Ci się zdobyć trofeum! - - - - Wstecz - Konfiguracja początkowa została ukończona. -System Vita3K jest gotowy! - Wybierz język. - Dalej - Wybierz preferowaną ścieżkę. - Vita3K wymaga pełnego dostępu do plików. -Dotknij 'Przyznaj dostęp', aby kontynuować. - Przyznaj dostęp - Obecna ścieżka emulatora - Zmień ścieżka emulatora - Resetuj ścieżka emulatora - Instaluj oprogramowanie. - Instalacja wszystkich plików oprogramowania jest wysoce zalecana. - Zainstalowana: - Pobierz paczkę czcionek - Zainstaluj plik oprogramowania systemu konsoli - Konfiguracja interfejsu graficznego. - Widoczność pasku informacyjnego - Zaznacz, aby pokazywać pasek informacyjny podczas wyboru aplikacji. -Pasek informacyjny to zegar, poziom naładowania baterii oraz centrum powiadomień. - Ekran aplikacji 'Live Area' - Zaznacz, aby otworzyć 'Live Area' domyślnie podczas kliknięcia na aplikacje. -Jeśli wyłaczone, wymaga prawego kliknięcia, aby uruchomić aplikacje. - Tryb siatki - Zaznacz, aby lista aplikacji była wyświetlana w trybie siatki, tak jak w konsoli PS Vita. - Rozmiar ikony aplikacji - Wybierz preferowany rozmiar ikon. - Ukończone. - - - - - Instalacja oprogramowania - Instalacja trwa, proszę czekać... - Oprogramowanie systemu zostało zainstalowane pomyślnie. - Wersja oprogramowania systemu: - Paczka czcionek nie została znaleziona, proszę pobierz i zainstaluj ją. - Paczka czcionek jest wymagana dla niektórych aplikacji, -również dla wsparcia regionalnych azjatyckich czcionek (Zalecane) - Usunąć plik oprogramowania systemu? - - - Wybierz typ licencji - Wybierz work.bin/rif - Wprowadź klucz zRIF - Wprowadź klucz zRIF - Proszę wprowadź twój klucz zRIF tutaj... - Ctrl + C, aby skopiować, Ctrl + V, aby wkleić. - Usunąć plik .pkg? - Usunąć plik work.bin/rif? - Nie udało się zainstalować pakietu. -Upewnij się, że pliki pkg oraz work.bin/rif lub klucz zRIF są poprawnie. - - - Wybierz typ instalacji - Wybierz plik - Wybierz katalog - {} archiwum/wów znalezionych ze wspieraną zawartości. - Pomyślnie zainstalowano {} archiwum/wów ze wspieraną zawartością: - Aktualizuj aplikację do: - Instalacja zawartości z {} archiwum/wów nie powiodła się: - Kompatybilna zawartość nie jest dostępna w {} archiwum/wach: - Usunąć archiwum? - - - Licencja została zainstalowana pomyślnie. - Instalacja licencji zakończyła się n. -Upewnij się, że pliki work.bin/rif lub klucz zRIF. - - - Zainstalować ponownie tą zawartość? - Ta zawartość jest już zainstalowana. - Czy chcesz ją zainstalować ponownie i nadpisać istniejące dane? - - - - - Rozpocznij - Kontynuuj - - Używa konfiguracji dla klawiatury w ustawieniach sterowania - Oprogramowanie nie zostało wykryte. Instalacja jest zalecana. - Czcionki oprogramowania nie zostały wykryte. Instalacja jest zalecana dla czcionek tekstu w trybie 'Live Area' - Pomoc 'Live Area' - Przeszukaj listę w aplikacji - Krzyżak (D-pad), Lewy drążek, kołko myszy w górę/dół lub za pomocą suwaka - Uruchom aplikacje - Naciśnij 'Uruchom' lub krzyżyk - Pokazuj/Ukrywaj 'Live Area' podczas uruchamiania aplikacji - Naciśnij przycisk PS - Wyjdź z 'Live Area' - Naciśnij 'Esc' lub kółko - Pomoc instrukcji użytkownika - Przeszukaj strone - Krzyżak (D-pad), Lewy drążek, kołko myszy w górę/dół lub za pomocą suwaka, Naciśnij na </> - Ukryj/pokaż przycisk - Lewy przycisk myszy lub wciśnij Trójkąt - Wyjdź z instrukcji - Naciśnij Esc lub wciśnij PS na kontrolerze - - - - - Nie udało się wczytać "{}". -Sprawdź plik vita3k.log, aby zobaczyć szczegóły na konsoli. -1. Czy zainstalowałeś oprogramowanie (firmware)? -2. Zrób ponowny zrzut swojej aplikacji/gry PS Vita i zainstaluj ją na Vita3K. -3. Instalacja lub uruchamianie Vitamin nie jest wspierane. - - - - Śred - Min - Maks - - - - - Domyślnie - - Znajdź własne motywy PSVita - - Nazwa - Dostawca - Zaktualizowano - Rozmiar - Wersja - Identyfikator zawartości - - Ten motyw zostanie usunięty. - - - Obraz - Dodaj obraz - - - Usuń tło - Dodaj tło - - - - - RRRR/MM/DD - DD/MM/RRRR - MM/DD/RRRR - - - Zegar 12-godz - Zegar 24-godz - - - - Język systemu - - - - Duński - Niemiecki - Angielski (Wielka Brytania) - Angielski (Stany Zjednoczone) - Hiszpański - Francuski - Włoski - Niderlandzki - Norweski - Polski - Portugalski (Brazylia) - Portugalski (Portugalia) - Rosyjski - Fiński - Szwedzki - Turecki - - - - - - - - - Tryb modułów - Lista modułów - Wybierz pożądane moduły. - Przeszukaj moduły - Wyczyść listę - Brak dostępnych modułów. -Proszę pobrać i zainstalować najnowsze oprogramowanie systemu PS Vita. - Odśwież listę - Automatyczny - Wybierz tryb Automatyczny, aby użyć domyślnej listy modułów. - Automatyczny i Ręczny - Wybierz ten tryb, aby wczytać moduły Automatyczne oraz te wybrane z listy poniżej. - Ręczny - Wybierz tryb Ręczny, aby wczytać tylko moduły wybrane z listy poniżej. - - - Włącz optymalizacje - Zaznacz, aby włączyć dodatkowe optymalizacje CPU JIT. - - - Resetuj - Sterownik renderowania - Automatyczny - Wybierz preferowany sterownik renderowania. - GPU (Wymaga restartu) - Wybierz kartę graficzną, jaką Vita3K powinno używać. - Standardowa - Wysoka - Dokładość renderowania - Synchronizacja pionowa - Wyłączenie synchronizacji pionowej może naprawić problem z prędkością w niektórych grach. -Zaleca się zostawić tą opcje włączoną, aby uniknąć rozdarcia obrazu. - Wyłącz synchronizację powierzchni - Poprawka prędkościowa, zaznacz, aby wyłączyć synchronizację powierzchni pomiędzy procesorem, a kartą graficzną. -Wymagane przez kilka gier. -Jeśli wyłączone, gwarantuje skok wydajności (dokładnie, gdy skalowanie w górę jest włączone). - Asynchroniczna kompilacja potoku - Pozwala na kompilacje potoków jednocześnie w wielu współbieżnych wątkach. -Zmniejsza to zacinanie się kompilacji potoku kosztem tymczasowych błędów graficznych. - Najbliższe - Dwuliniowe - Dwusześcienne - Filtrowanie ekranu - Ustaw filtr przetwarzania końcowego, który ma zostać zastosowany. - Skalowanie rozdzielczości - Pozwala na podbicie rozdzielczości w Vita3K. -Eksperymentalne: Nie ma gwarancji, że gry będą renderować się poprawnie na innej niż 1x. - Filtrowanie anizotropowe - Filtrowanie anizotropowe to technika w celu poprawy jakości obrazu powierzchni -które są nachylone w stosunku do widza. -Nie ma to żadnych wad, ale może mieć wpływ na wydajność. - Podmiana tekstur - Eksportuj tekstury - Importuj tekstury - Format eksportowanych tekstur - Cienie - Używaj pamięć podręczną cieni - Zaznacz, aby włączyć pamięć podręczną cieci, aby skompilować je na starcie gry. -Odznacz, aby wyłączyć tą funkcje. - Używaj cieni Spir-V (porzucone) - Przekaż wygenerowany moduł cieniujący Spir-V bezpośrednio do sterownika. -Pamiętaj, że niektóre przydatne rozszerzenia zostaną wyłączone, -i nie wszystkie procesory graficzne są z tym kompatybilne. - Wyczyść pamięć podręczną i dziennik cieni - FPS Hack - Hack do gry, który pozwala niektórym grom działającym z szybkością 30 FPS działać w 60 FPS na emulatorze. -Pamiętaj, że jest to hack i będzie działać tylko w niektórych grach. -W innych grach może to nie mieć żadnego efektu lub spowodować, że będą działać dwa razy szybciej. - - - - Przypisanie przycisku potwierdzenia -Wybierz swój przycisk potwierdzenia. - Jest to przycisk używany jako 'Potwierdź' in oknie aplikacji. -Niektóre aplikacje tego nie używają, mają swój przycisk potwierdzenia. - Kółko - Krzyżyk - Tryb PS TV - Zaznacz, aby włączyć emulowany tryb PS TV. - Tryb Show - Zaznacz, aby włączyć tryb Show. - Tryb demonstracyjny - Zaznacz, aby włączyć tryb demonstracyjny. - - - Uruchamiaj aplikacje w trybie pełnoekranowym - Śledź - Informacje - Ostrzeżenia - Błędy - Krytyczne - Wyłącz - Poziom rejestrowania pliku dziennika - Wybierz prefererowany poziom zbierania danych w dzienniku. - Archiwizuj plik dziennika - Zaznacz, aby archiwizować plik dziennika. - Włącza Discord Rich Presence, aby pokazać na Discordzie jaką aplikacje uruchamiasz. - Pamięć podręczna tekstur - Odznacz, aby wyłączyć pamięć podręczną tekstur. - Pokazuj kompilacje cieni - Odznacz, aby wyłączyć informacje w oknie dialogowym o kompilacji cieni. - Pokazuj kursor ekranu dotykowego - Odznacz, aby wyłączyć widoczność kursora ekranu dotykowego. - Loguj ostrzeżenie o kompatybilności - Zaznacz,aby włączyć logowanie ostrzeżenie o kompatybilności dla 'Issue' na GitHubie. - Sprawdź aktualizacje - Automatycznie sprawdzaj aktualizację przy uruchomieniu. - Nakładka wydajnościowa - Wyświetla informację wydajnościową na ekranie jako nakładkę. - Minimum - Niski - Średni - Maksimum - Poziom detali - Wybierz preferowany poziom wyświetlanych detali w nakładce wydajnościowej. - Lewy górny - Wyśrodkowanie górne - Prawy górny - Lewy dolny - Wyśrodkowanie dolne - Prawy dolny - Pozycja nakłądki - Wybierz preferowaną pozycję nakładki wydajnościowej. - Zaznacz, aby umożliwić wyszukiwanie scieżek bez uwzględnienia wielkości liter dla systemu z uwzględnianiem wielkości liter. -RESETUJE SIĘ PRZY PONOWNYM URUCHOMIENIU - Pozwala emulatorowi, aby podjąć szukanie plików niezalenie od przypadku -na platformach innych niż Windows. - Folder emulowanej pamięci systemu - Obecna ścieżka emulatora: - Zmień ścieżkę emulatora - Zmienia ścieżkę do folderu emulatora Vita3K. -Wymaga ręcznego przeniesienia folderu do nowej lokalizacji. - Resetuj ścieżkę emulatora - Resetuje ścieżkę do folderu emulatora Vita3K do ustawień domyślnych -Wymaga ręcznego przeniesienia folderu do nowej lokalizacji. - Ustawienia niestandardowej konfiguracji - Wyczyść niestandardową konfigurację - Zrzuty ekranu - Brak - Format zrzutu ekranu - - - Włącz interfejs graficzny - Zaznacz tę opcję, aby pokazywać interfejs graficzny, po uruchomienu aplikacji. - Włącz pasek informacyjny - Zaznacz, aby pokazywać pasek informacyjny przy wyborze aplikacji. - Język interfejsu graficznego - Wybierz język interfejsu użytkownika. - Wyświetlaj pasek informacyjny - Odznacz, aby zapisywać pasek informacyjny tylko w pliku dziennika. - Wyświetlaj aplikacje systemowe - Odznacz, aby schować aplikacje systemowe z ekranu startowego. -Będą dalej widoczne w menu głównym. - Ekran aplikacji 'Live Area' - Zaznacz, aby domyślnie otwierać 'Live Area' po kliknięciu aplikacji. -Jeśli jest wyłączona, kliknij aplikację prawym przyciskiem myszy, aby ją otworzyć. - Rozciągnij obraz do obszaru wyświetlania - Zaznacz, aby powiększyć obszar wyświetlania w celu dopasowania go do rozmiaru ekranu. - Pełny ekran w rozdzielczości HD (idealne piksele) - Zaznacz, aby uzyskać idealne renderowanie pikseli w rozdzielczościach HD (1080p, 4K) na pełnym ekranie -kosztem niewielkiego przycięcia u góry i u dołu ekranu. - Tryb siatki - Zaznacz aby wyświetlać listę aplikacji w trybie siatki. - Rozmiar ikony aplikacji - Definiuje preferowany rozmiar ikon aplikacji. - Wsparcie dla czcionek - Region azjatycki - Zaznacz, aby włączyć obsługę czcionek w języku chińskim i koreańskim. -Włączenie tej opcji spowoduje użycie większej ilości pamięci i będzie wymagało ponownego uruchomienia emulatora. - Paczka czcionek oprogramowania jest potrzebna dla niektórych aplikacji -oraz dla wsparcia azjatyckich czcionek regionalnych w graficznym interfejsie użytkownika. -Jest też głównie polecenana dla graficznym interfejsie użytkownika. - Motyw i tło - Identyfikator obecnego motywu: - Resetuj domyślny motyw - Obecnie używane tło motywu - Wyczyść tła użytkownika - Obecne tło startowe: - Restuj tło startowe - Tło Alpha - Definiuje preferowaną przeźroczystość tła. -Wartość minimalna jest nieprzeźroczysta, a wartość minimalna jest przeźroczysta. - Opóźnienie dla zmiany teł - Definiuje opóźnienie (w sekundach) przed zmianą teł. - Opoźnienie dla ekranu startowego - Definiuje opóźnienie (w sekundach) przed powrotem do ekranu startowego. - - - Zalogowany do PSN - Jeśli zaznaczone, gry będą zakładać że użytkownik jest połączony z PlayStation Network (ale w trybie offline). - Adres IP - Maska podsieci - Włącz HTTP - Zaznacz, aby umożliwić grom korzystanie z protokołu HTTP w internecie. - Próby przekroczenia czasu połączenia HTTP - Ilość prób połączenia do wykonania, gdy serwer nie odpowiada. -Może być użyteczne, jeśli posiadasz NIESTABILNE lub BARDZO WOLNE połączenie internetowe. - Uśpienie przekroczenia czasu połączenia HTTP - Nakłada uśpienie między próbami połączenia z serwerem, gdy nie odpowiada. -Może być użyteczne, jeśli posiadasz NIESTABILNE lub BARDZO WOLNE połączenie internetowe. - Próby zakończenia odczytu HTTP - Ilość prób do wykonania, gdy nie ma więcej danych do odczytu, -niższa wartość może poprawić wydajność, ale może sprawić, że gry będą niestabilne, jeśli masz naprawdę słabe połączenie z internetem. - Uśpienie przy próbach zakończenia odczytu HTTP - Nakłada uśpienie, gdy nie ma więcej danych do odczytu, -niższa wartość może poprawić wydajność, ale może sprawić, że gry będą niestabilne, jeśli masz naprawdę słabe połączenie z internetem. - - - Importuj dziennik zdarzeń - Loguj moduły importowanych symboli. - Eksportuj dziennik zdarzeń - Loguj moduły eksportowanych symboli. - Loguj aktywne cienie - Loguj cienie używane przy każdym wywołaniu rysowania. - Jednolity dziennik zdarzeń - Loguj jednolite nazwy i wartości cieni. - Zapisz powierzchnie kolorów - Zapisz powierzchnie kolorów do plików. - Zrzut do pliku ELF - Zrzuć załadowany kod do pliku/ów ELFs. - Warstwa walidacji (Wymaga restartu) - Włącz warstwę walidacji Vulkan. - Przestań obserwować kod - Obserwuj kod - Przestań obserwować pamięć - Obserwuj pamięć - Przestań obserwować wywołania importu - Obserwuj wywołania importu - - Zapisz i uruchom ponownie - Zapisz i zatwierdź - Zapisz i zamknij - Naciśnij 'Zapisz', aby zachować zmiany. - - - - Przeglądarka - Przeglądarka internetowa - Trofea - Kolekcja trofeów - Ustawienia - Menedźer zawartości - - - - Usuń trofeum - Ta informacja o trofeum przypisana dla tego użytkownika zostanie usunięta. - Niezdobyto -
Szczegóły
- Zdobyto - Nazwa - Brak trofeów. -Trofea można zdobywać, korzystając z aplikacji obsługującej funkcję trofeów. - Nie zdobyto - Oryginalne - Sortuj - Trofea - Stopień - Postęp - Zaktualizowano - Dalej - Pokaż ukryte trofea -
- - - Zaznacz użytkownika - Utwórz użytkownika - Został utworzony następujący użytkownik: - Edytuj użytkownika - Otwórz folder użytkownika - Ta nazwa jest już używana. - Usuń użytkownika - Wybierz użytkownika, którego chcesz usunąć. - Następujący użytkownicy zostaną usunięci: - Jeśli usuniesz użytkownika, usunięte jego zapisane dane, trofea. - Użytkownik zostanie usunięty. -Czy na pewno chcesz kontynuować? - Usunięto użytkownika. - Wybierz awatar - Resetuj awatar - Uzytkownik - Potwierdź - Automatyczne logowanie - - - - Dostępna jest nowa wersja Vita3K. - Wstecz - Czy chcesz anulować tę aktualizację? -Jeśli operacja zostanie anulowana, przy następnej aktualizacji Vita3K rozpocznie pobieranie od tego miejsca. - Pobieranie... -Po ukończeniu pobierania Vita3K zostanie automatycznie ponownie uruchomiony, a następnie zostanie zainstalowane nowe aktualizacji. - Nie można ukończyć aktualizacji. - Pozostało {} min - Dalej - Pozostało {} s - Czy chcesz zaktualizować Vita3K? - Zainstalowana jest już nowsza wersja Vita3K. - Najnowsza wersja Vita3K jest już zainstalowana. - Nowe funkcje w wersji {} - Autor/zy - Komentarze - Aktualizuj - Wersja {} - - - - Vita3K, emulator systemu PlayStation Vita - Vita3K jest otwarto-źródłowym emulatorem PlayStation Vita pisanym w języku C++ dla systemu Windows, Linux, macOS oraz Android. - Emulator jest wciąż w fazie rozwojowej, więc każda informacja zwrotna w formie testów jest mile widziana. - Aby rozpocząć, zainstaluj wszystkie pliki oprogramowania PS Vita. - Pobierz oprogramowanie Preinst - Pobierz oprogramowanie systemu konsoli - Pobierz paczkę czcionek - Szczegółowy poradnik konfiguracji Vita3K jest dostępny na stronie - 'Quickstart' - . - Sprawdź listy kompatybilności z aplikacjami komercyjnymi i homebrew, aby zobaczyć co obecnie działa. - Lista kompatybilności z aplikacjami komercyjnymi - Lista kompatybilności z homebrew - Pomoc przy projekcie mile widziana! - Dodatkowe wsparcie i pomoc dostępne są również na Discordzie na kanale #help. - Vita3K nie wspiera piractwa. Musisz zgrać swoją kopię gry. - Pokaż następnym razem - -
diff --git a/lang/system/pt-br.xml b/lang/system/pt-br.xml deleted file mode 100644 index ae377608c..000000000 --- a/lang/system/pt-br.xml +++ /dev/null @@ -1,489 +0,0 @@ - - - - - - Abrir diretório das configurações - Instalar firmware - Instalar .pkg - Instalar .zip, .vpk - Instalar licença - Sair - - - Últimos apps usados - - - Gerenciamento de usuários - - - Controles do teclado - - - Bem-vindo - - - - - Vita3K: Um emulador de PS Vita/PS TV. O primeiro emulador funcional de PS Vita/PS TV do mundo. - Vita3K é um emulador open-source experimental de PlayStation Vita/PlayStation TV escrito em C++ para Windows, Linux, macOS e Android. - Se você está interessado em contribuir, visite nosso: - Visite nosso website para mais informações: - Equipe Vita3K - Desenvolvedores - Contribuidores - Apoiantes - - - - - Atualizar Database - - - Nome e ID do título - Sumário de aplicativos - - - Criar - Editar - Remover - - - Abrir pasta - Licença - Cache de Shader - Log de Shader - - - Manual - Atualização - - - Esse aplicativo e todos os dados relacionados, inclusive dados salvos, serão excluídos. - Deletar um aplicativo pode demorar algum tempo -dependendo de seu tamanho e hardware. - - Histórico de atualizações - Versão {} - - Qualificado - Não qualificado - Nível {} - Nome - Conquista de troféus - Controle parental - Atualizado - Tamanho - Versão - ID do título - Usado pela última vez - Tempo usado - Nunca - - - - - Ocorreu um erro. -Código de erro: {} - Cancelar - Fechar - Não foi possível carregar o arquivo. - Não foi possível salvar o arquivo. - Excluir - Erro - O arquivo está corrompido. - Ative o microfone. - Não - Aguarde... - Pesquisar - Selecionar tudo - - Enviar - Sim - - domingo - segunda - terça - quarta - quinta - sexta - sábado - - - janeiro - fevereiro - março - abril - maio - junho - julho - agosto - setembro - outubro - novembro - dezembro - - - janeiro - fevereiro - março - abril - maio - junho - julho - agosto - setembro - outubro - novembro - dezembro - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 de - 2 de - 3 de - 4 de - 5 de - 6 de - 7 de - 8 de - 9 de - 10 de - 11 de - 12 de - 13 de - 14 de - 15 de - 16 de - 17 de - 18 de - 19 de - 20 de - 21 de - 22 de - 23 de - 24 de - 25 de - 26 de - 27 de - 28 de - 29 de - 30 de - 31 de - - Troféu escondido - 1 hora atrás - 1 minuto atrás - Bronze - Ouro - Platina - Prata - {} horas atrás - {} minutos atrás - - - - - Desconhecido - Nada - Inicializável - Intro - Menu - No jogo - - No jogo + - Jogável - - - - - - Os aplicativos selecionados e todos os dados relacionados, inclusive dados salvos, serão excluídos. - Não há nenhum itens de conteúdo. - - - Os itens de dados salvos selecionados serão excluídos. - Não há dados salvos. - - Tema - Espaço livre - Limpar tudo - - - - {} controles conectados - Núm - Nenhum controle compatível conectado. -Conecte um controle que seja compatível com SDL3. - - - - - Preparando para iniciar o aplicativo... - - - - Deseja cancelar a exclusão? - Exclusão concluída. - Deseja excluir esses dados salvos? - - -
Detalhes
- Atualizado -
- - Deseja cancelar o carregamento? - Não há dados salvos. - Carregamento concluído. - Carregando... - Deseja carregar esses dados salvos? - - - Deseja cancelar o salvamento? - Não foi possível salvar o arquivo. -Não há espaço livre suficiente no cartão de memória. Para salvar o seu progresso no aplicativo, crie pelo menos {} de espaço livre. - -Para criar espaço livre, pressione o botão PS para pausar esse aplicativo e exclua outros aplicativos ou conteúdo. - Não há espaço livre suficiente no cartão de memória. -Para continuar usando o aplicativo, crie pelo menos {} de espaço livre. - -Pressione o botão PS para pausar o aplicativo e exclua outros aplicativos ou conteúdo. - Novos dados salvos - Gravação concluída. - Deseja salvar os dados? - Salvando... - Salvando... -Não desligue o sistema nem feche o aplicativo. - Deseja substituir esses dados salvos? - -
-
- - - O seguinte aplicativo será fechado. - Dados a serem excluídos: - - - - O aplicativo foi adicionado à tela principal. - Excluir tudo - As notificações serão excluídas. - Não foi possível instalar. - Instalação concluída. - Instalando... - Não há notificações. - Você conquistou um troféu! - - - - Voltar - Você concluiu a configuração inicial. -O seu sistema Vita3K está pronto! - Selecione um idioma. - Vita3K requer acesso completo aos arquivos. -Toque em 'Conceder Acesso' para continuar. - Conceder Acesso - Instalar firmware. - Instalado: - Baixar Pacote de Fonte - Instalar Arquivo de Firmware - Completo. - Próximo - - - - Iniciar - Continuar - - Firmware não detectado. A instalação é altamente recomendada - Pacote de fonte não detectado. Sua instalação é recomendada para o texto da fonte na tela inicial - Ajuda da Live Area - Navegar na lista de aplicativos - Iniciar aplicativo - Mostrar/Esconder a tela inicial -durante o uso do aplicativo - Pressione PS - Sair da Live Area - Clique no Esc ou pressione Circulo - Navegar página - Esconder/Mostrar botão - Clique Direito ou Pressione Triângulo - Clique no Esc ou Pressione PS - - - - - - Padrão - - - Nome - Provedor - Atualizado - Tamanho - Versão - ID do Conteúdo - - Esse tema será excluído. - - - Imagem - Adicionar imagem - - - Deletar plano de -fundo - Adicionar plano de -fundo - - - - - AAAA/MM/DD - DD/MM/AAAA - MM/DD/AAAA - - - Relógio de 12 horas - Relógio de 24 horas - - - - Idioma do sistema - - - - Dinamarquês - Alemão - Inglês (Reino Unido) - Inglês (Estados Unidos) - Espanhol - Francês - Italiano - Holandês - Norueguês - Polaco - Português (Brasil) - Português (Portugal) - Russo - Finlandês - Sueco - Turco - - - - - - - - - - - Endereço IP - Máscara de sub-rede - - Salvar & Fechar - - - - Navegador - Navegador de Internet - Troféus - Coleção de troféus - Configurações - Gerenciador de conteúdo - - - - Deletar troféu - A informação deste troféu salva neste usuário irá ser deletada. -
Detalhes
- Conquistado - Nome - Não há nenhum troféu. -Você pode conquistar troféus usando um aplicativo compatível com o recurso de troféus. - Não conquistado - Original - Classificar - Troféus - Grau - Progresso - Atualizado -
- - - Selecionar usuário - Criar usuário - O usuários a seguir foi criado: - Editar usuário - O nome já está sendo usado. - Excluir usuário - Selecione o usuário que você deseja excluir. - O usuário a seguir será excluído: - Se você excluir o usuário, os dados salvos, troféus desse usuário serão excluídos. - O usuário será excluído. -Tem certeza de que deseja continuar? - Usuário excluído. - Escolher avatar - Resetar avatar - Usuario - Confirmar - Login Automático de Usuário - - - - Uma nova versão Vita3K está disponível. - Voltar - Deseja cancelar a atualização? -Se cancelar a operação, na próxima atualização, o Vita3K iniciará o download desse ponto. - Fazendo download... -Quando o download terminar, Vita3K será reiniciado automaticamente e o novo atualização será instalado. - Não foi possível concluir a atualização. - Faltam {} minutos - Próximo - Faltam {} segundos - Deseja atualizar o Vita3K? - A versão posterior Vita3K já está instalada. - A versão mais recente Vita3K já está instalada. - Novos recursos da versão {} - Atualizar - Versão {} - - - - Baixar Firmware - -
diff --git a/lang/system/pt.xml b/lang/system/pt.xml deleted file mode 100644 index f68cedfff..000000000 --- a/lang/system/pt.xml +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - Open Pref Path - Install Firmware - Install .pkg - Install .zip, .vpk - Install License - Sair - - - Last Apps used - - - Gestão de utilizadores - - - Keyboard Controls - - - Welcome - - - - - - Manual - Atualização - - - Esta aplicação e todos os dados relacionados, incluindo dados guardados, serão elminados. - - Histórico de atualização - Versão {} - - Elegível - Inelegível - Nível {} - Nome - Ganho de Troféus - Controlo parental - Atualizado - Tamanho - Versão - Usado pela última vez - Tempo usado - Nunca - - - - - Ocorreu um erro. -Código de erro: {} - Cancelar - Fechar - Não foi possível carregar o ficheiro. - Não foi possível guardar o ficheiro. - Eliminar - Erro - O ficheiro está corrompido. - Ative o microfone. - Não - Por favor aguarde... - Selecionar Todos - - Sim - - domingo - segunda-feira - terça-feira - quarta-feira - quinta-feira - sexta-feira - sábado - - - Janeiro, - Fevereiro, - Março, - Abril, - Maio, - Junho, - Julho, - Agosto, - Setembro, - Outubro, - Novembro, - Dezembro, - - - Janeiro - Fevereiro - Março - Abril - Maio - Junho - Julho - Agosto - Setembro - Outubro - Novembro - Dezembro - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 de - 2 de - 3 de - 4 de - 5 de - 6 de - 7 de - 8 de - 9 de - 10 de - 11 de - 12 de - 13 de - 14 de - 15 de - 16 de - 17 de - 18 de - 19 de - 20 de - 21 de - 22 de - 23 de - 24 de - 25 de - 26 de - 27 de - 28 de - 29 de - 30 de - 31 de - - Troféu oculto - Há 1 hora - Há 1 minuto - Bronze - Ouro - Platina - Prata - Há {} horas - Há {} minutos - - - - - As aplicações selecionadas e todos os dados relacionados, incluindo dados guardados, serão eliminados. - Não há itens de conteúdo. - - - Os itens de guardados selecionados serão eliminados. - Não há dados guardados. - - Tema - Espaço livre - Limpar todos - - - - Núm - - - - - A preparar para iniciar a aplicação... - - - - Pretende cancelar a eliminação? - Eliminação concluída. - Pretende eliminar estes dados guardados? - - -
Detalhes
- Atualizado -
- - Pretende cancelar o carregamento? - Não existem dados guardados. - Carregamento concluído. - A carregar... - Pretende carregar estes dados guardados? - - - Pretende cancelar a operação de guardar? - Não foi possível guardar o ficheiro. -Não existe espaço livre suficiente no cartão de memória. Não existe espaço livre suficiente no cartão de memória. Para guardar o progresso na aplicação, tem de criar, pelo menos, {} de espaço livre. - -Para criar o espaço livre, prima o botão PS para pausar esta aplicação e, em seguida, elimine outras aplicações ou conteúdo. - Não existe espaço livre suficiente no cartão de memória. -Para continuar a utilizar a aplicação, tem de criar, pelo menos, {} de espaço livre. - -Prima o botão PS para pausar esta aplicação e, em seguida, elimine outras aplicações ou conteúdo. - Novos dados guardados - Guardado. - Pretende guardar os dados? - A guardar... - A guardar... -Não desligue o sistema nem feche a aplicação. - Pretende gravar por cima destes dados guardados? - -
-
- - - A seguinte aplicação será fechada. - Dados a serem eliminados: - - - - A aplicação foi adicionada ao ecrã de início. - Elimine todos - As notificações serão eliminadas. - Não foi possível instalar. - Instalação concluída. - A instalar... - Não existem notificações. - Ganhaste um troféu! - - - - Retroceder - Concluiu a configuração inicial. -O seu sistema Vita3K está pronto! - Selecione um idioma. - Seguinte - - - - Iniciar - Continuar - - - - - Predefinição - - - Nome - Fornecedor - Atualizado - Tamanho - Versão - - Este tema será eliminado. - - - Imagem - - - - - - - AAAA/MM/DD - DD/MM/AAAA - MM/DD/AAAA - - - Relógio de 12 horas - Relógio de 24 horas - - - - Idioma do sistema - - - - Dinamarquês - Alemão - Inglês (Reino Unido) - Inglês (Estados Unidos) - Espanhol - Francês - Italiano - Holandês - Norueguês - Polaco - Português (Brasil) - Português (Portugal) - Russo - Finlandês - Sueco - Turco - - - - - - - - - - - Endereço IP - Máscara de sub-rede - - Guardar & Fechar - - - - Navegador - Navegador de Internet - Troféus - Coleção de troféus - Definições - Gestor de conteúdo - - - -
Detalhes
- Ganho - Nome - Não existem troféus. -Pode ganhar troféus utilizando uma aplicações que suporte a funcionalidade de troféus. - Sem ganhos - Original - Ordenar - Troféus - Grau - Progresso - Atualizado -
- - - Selecionar utilizador - Criar utilizador - O seguinte utilizador foi criado: - Editar utilizador - O nome já está a ser utilizador. - Eliminar utilizador - Selecione o utilizador que pretende eliminar. - O utilizador seguinte será eliminado: - Se eliminar o utilizador, os dados guardados, troféus desse utilizador serão eliminados. - O utilizador será eliminado. -Tem a certeza de que pretende continuar? - Utilizador eliminado. - Escolher avatar - Utilizador - Confirmar - - - - Está disponível uma nova versão da Vita3K. - Retroceder - Pretende cancelar a atualização? -Se cancelar a operação, da próxima vez que atualizar, o Vita3K iniciará a transferência a partir deste ponto. - A transferir... -Uma vez concluída a transferência, Vita3K será reiniciado automaticamente e, em seguida, instalará o novo atualização. - Não foi possível concluir a atualização. - Restam {} minutos - Seguinte - Restam {} segundos - Pretende atualizar o Vita3K? - Já está instalada a versão mais recente Vita3K. - A última versão Vita3K já está instalada. - Novas funcionalidades na versão {} - Atualizar - Versão {} - -
diff --git a/lang/system/ru.xml b/lang/system/ru.xml deleted file mode 100644 index dfb0b7137..000000000 --- a/lang/system/ru.xml +++ /dev/null @@ -1,890 +0,0 @@ - - - - - - Открыть предпочтенный путь файлов - Установить прошивку - Установить файл .pkg - Установить файл .zip или .vpk - Установить лицензию - Выход - - - Последние использованные приложения - - - - - Управление пользователями - - - Элементы управления клавиатурой - - - Добро пожаловать - - - - - Vita3K: эмулятор PS Vita/PS TV. Первый в мире функциональный эмулятор PS Vita/PS TV. - Vita3K - это экспериментальный эмулятор PlayStation Vita/PlayStation TV с открытым исходным кодом, написанный на C++ для операционных систем Windows, Linux, macOS и Android. - Особое спасибо: создатель иконки Vita3K: - Если вы заинтересованы в том, чтобы внести свой вклад, загляните в наш - Если вы хотите поддерж. нас, то вы можете сделать пожертвование или оформ. подписку: - Посетите наш сайт для большей информации: - Команда Vita3K - Разработчики - Помощники - Подписчики Ko-fi - - - - - Проверить совместимость приложения - Скопировать Vita3K Summary - Открыть отчёт о совместимости - Создать отчёт о совместимости - Обновить базу данных - - - Название и Title ID - Информация вкратце - - - Создать - Изменить - Удалить - - - Открыть папку - Лицензия - Кэш шейдеров - Лог шейдеров - Экспорт текстур - Импорт текстур - - - Руководство - Обновление - - - Это приложение и все связанные с ним данные, в том числе сохраненные данные, будут удалены. - Удаление приложения может занять некоторое время -в зависимости от его размера и производительности вашей системы. - Удалить DLC? - Удалить лицензию? - - Хронология обновлений - Версия {} - - Допущен - Не допущен - Уровень {} - Название - Полученные призы - Родительский контроль - Обновлен - Размер - Версия - Категория - Время последнего использования - Время использования - Никогда - - - - - Произошла ошибка. -Код ошибки: {} - Отмена - Закрыть - Не удалось загрузить файл. - Не удалось сохранить файл. - Удалить - Ошибка - Файл поврежден. - Включите микрофон. - Нет - Подождите... - Поиск - Выбрать все - - Подтвердить - Да - - воскресенье - понедельник - вторник - среда - четверг - пятница - суббота - - - Январь, - Февраль, - Март, - Апрель, - Май, - Июнь, - Июль, - Август, - Сентябрь, - Октябрь, - Ноябрь, - Декабрь, - - - января - февраля - марта - апреля - мая - июня - июля - августа - сентября - октября - ноября - декабря - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Скрытый приз - 1 час назад - 1 мин. назад - Бронза - Золото - Платина - Серебро - {} час. назад - {} мин. назад - - - - - Неизвестно - Не запускается - Запускается - Заставка - Меню - В игре - - В игре + - Играбельно - - - - - Не удалось получить текущую базу данных совместимости, проверьте брандмауэр/доступ к Интернету, повторите попытку позже. - Не удалось загрузить базу данных совместимости приложений, обновленную на: {}, повторите попытку позже. - Не удалось открыть базу данных совместимости приложений, обновленную на: {}. - База данных совместимости была успешно обновлена с {} до {}. - -В списке появилось {} новых приложений, в результате чего общее количество приложений достигло {}! - База данных совместимости была успешно обновлена с {} до {}. - -В списке {} приложений! - База данных совместимости, обновленная в {}, была успешно загружена. - -{} приложений в списке! - - - - Компиляция шейдеров - {} конвейеров скомпилировано - {} шейдеров скомпилировано - - - - - Выбранные приложения и все связанные с ними данные, в том числе сохраненные данные, будут удалены. - Отсутствуют элементы данных. - - - Выбранные сохраненные данные будут удалены. - Отсутствуют сохраненные данные. - - Тема - Cвободное - Очистить все - - - - Контроллеров подключено: {} - Количество - Перепривязка элементов управления - Цвет светодиода - Использовать пользовательский цвет - Установите этот флажок, чтобы использовать пользовательский цвет для светодиода контроллера. - Красный - Зеленый - Синий - Не подключены совместимые контроллеры. -Подключите контроллер, совместимый с SDL3. - Сброс привязки элементов управления - - - - - Привязанная кнопка - Левый cтик вверх - Левый cтик вниз - Левый cтик вправо - Левый cтик влево - Правый cтик вверх - Правый cтик вниз - Правый cтик вправо - Правый cтик влево - D-pad вверх - D-pad вниз - D-pad вправо - D-pad влево - Квадрат - Крестик - Кружок - Треугольник - Кнопка Start - Кнопка Select - Кнопка PS - Кнопка L1 - Кнопка R1 - Только в режиме PS TV. - Кнопка L2 - Кнопка R2 - Кнопка L3 - Кнопка R3 - Интерфейс - Полный экран - Переключение сенсора - Переключение между задним сенсором и сенсорным экраном. - Перекл. видим. интерф. - Переключает видимость интерфейса в верхней части экрана при работе приложения. - Разное - Перекл. подмены текстур - Сделать скриншот - Данная кнопка уже назначена. - - - - - Подготовка к запуску приложения... - - - - Отменить удаление? - Удаление завершено. - Удалить эти сохраненные данные? - - -
Подробности
- Обновлен -
- - Отменить загрузку? - Отсутствуют сохраненные данные. - Загрузка завершена. - Идет загрузка... - Загрузить эти сохраненные данные? - - - Отменить сохранение? - Не удалось сохранить файл. -На карте памяти недостаточно свободного места. Для сохранения процента выполнения приложения необходимо не менее {} свободного места. - -Для освобождения места нажмите кнопку PS, чтобы приостановить приложение, и удалите другие приложения или данные. - На карте памяти недостаточно свободного места. -Чтобы продолжить использование приложения, необходимо не менее {} свободного места. - -Нажмите кнопку PS, чтобы приостановить приложение, и удалите другие приложения или данные. - Новые сохраненные данные - Сохранение завершено. - Сохранить данные? - Идет сохранение... - Идет сохранение... -Не выключайте систему и не закрывайте приложение. - Перезаписать сохраненные данные? - -
-
- - - Следующее приложение будет закрыто. - Удаляемые данные: - - - - Фильтр - Упорядочить прил. по - Все - По региону - США - Европа - Япония - Азия - По типу - Коммерческие - Homebrew - По статусу совместимости - Верс. - Кат. - Совм. - Посл. запуск - Обнов. - - - - Приложение добавлено на начальный экран. - Удалить все - Уведомления будут удалены. - Не удалось выполнить установку. - Установка завершена. - Идет установка... - Уведомления отсутствуют. - Вы получили приз! - - - - Назад - Теперь начальная установка завершена. -Ваша система Vita3K готова! - Выбрать язык. - Выбрать путь к ФС эмулятора - Текущий путь к ФС эмулятора - Сменить путь к ФС эмулятора - Сбросить путь к ФС эмулятора - Установить прошивку. - Установлено: - Скачать пакет шрифтов - Установить прошивку - Выбрать настройки интерфейса. - Показывать инфопанель - Установите флажок, чтобы показать информационную панель внутри селектора приложений. -В информационной панели отображаются часы, уровень заряда батареи и центр уведомлений. - Показывать Live Area - Установите флажок, чтобы по умолчанию открывать Live Area при нажатии на приложение. -Если отключено, щелкните правой кнопкой мыши на приложении, чтобы открыть Live Area. - Режим сетки - Установите флажок, чтобы перевести список приложений в режим сетки, как на PS Vita. - Размер значков приложений - Выберите желаемый размер значков. - Готово. - Далее - - - - - Установка прошивки - Идет установка, пожалуйста, подождите... - Прошивка успешно установлена. - Версия прошивки: - Пакет шрифтов прошивки отсутствует, пожалуйста, скачайте и установите его. - Пакет шрифтов прошивки нужен для некоторых приложений, -а также для поддержки шрифтов азиатского региона. -(В целом рекомендуется) - Удалить установочный файл прошивки? - - - Выбрать тип лицензии - Выбр. work.bin/rif - Введите zRIF - Введите zRIF ключ - Пожалуйста, введите свой zRIF здесь - Ctrl + C чтобы копировать, Ctrl + V вставить. - Удалить файл pkg? - Удалить файл work.bin/rif? - Не удалось установить пакет. -Пожалуйста, проверьте файл pkg и work.bin/rif или zRIF ключ. - - - Выберите тип установки - Выбрать файл - Выбрать папку - Найдены {} архив(ов) с совместимым контентом. - Содержимое {} архива(ов) успешно установлено: - Обновить приложение до: - Не удалось установить содержимое {} архива(ов): - Не найден совместимый контент в {} архиве(ах):: - Удалить архив? - - - Лицензия успешно установлена. - Не удалось установить лицензию. -Пожалуйста, проверьте файл work.bin/rif или zRIF ключ. - - - Переустановить данный контент? - Данный контент уже установлен. - Вы хотите переустановить его и перезаписать уже существующие данные? - - - - - Запуск - Продолжить - - Используя конфигурацию для клавиатуры в настройках управления - Прошивка не найдена. Настоятельно рекомендуется установить её - Шрифт прошивки не найден. Установка рекомендуется для текста в Live Area - Справка Live Area - Просмотр списка -приложений - D-pad, л. стик, колёсико вверх/вниз -или используя ползунок - Запуск приложения - Нажать на Start или крестик - Показать/скрыть Live Area при работе приложения - Нажать PS - Выйти из Live Area - Нажать Esc или кружок - Справка по мануалу - Просмотр -страницы - D-pad, л. стик влево/вправо, колёсико вверх/вниз -либо нажав </> - Скрыть/показать кнопки - Левая кнопка мышки или нажать треугольник - Закрыть мануал - Нажать на Esc или PS - - - - - Не удалось загрузить "{}". -Проверьте vita3k.log, чтобы увидеть вывод консоли для получения подробной информации. -1. Установлена ли у вас прошивка? -2. Скопируйте собственное приложение(я)/игру(ы) и установите его на Vita 3K. -3. Если вы хотите установить или запустить Vitamin, он не поддерживается. - - - Сред - Мин - Макс - - - - - По умолчанию - - Найти пользовательские темы для PSVita - - Название - Поставщик - Обновлен - Размер - Версия - - Эта тема будет удалена. - - - Изображение - Добавить -изображение - - - Удалить фон - Добавить фон - - - - - ГГГГ/ММ/ДД - ДД/ММ/ГГГГ - ММ/ДД/ГГГГ - - - 12-часовой формат - 24-часовой формат - - - - Язык системы - - - - Датский - Немецкий - Английский (Великобритания) - Английский (США) - Испанский - Французский - Итальянский - Голландский - Норвежский - Польский - Португальский (Бразилия) - Португальский (Португалия) - Русский - Финский - Шведский - турецкий - - - - - - - - - Режим загрузки модулей - Список модулей - Выберите нужные модули. - Поиск модулей - Очистить список - Модули не найдены. -Пожалуйста, загрузите и установите последнюю прошивку PS Vita. - Обновить список - Автоматический - Выберите Автоматический чтобы использовать встроенный список модулей. - Авто и ручной - Выберите этот режим для загрузки автоматических модулей и выбранных модулей из списка ниже. - Ручной - Выберите Ручной режим для загрузки только выбранных модулей из списка ниже. - - - Включить оптимизацию - Установите флажок, чтобы включить дополнительные JIT-оптимизации процессора. - - - Сброс - Бэкенд рендерера - Выберите бэкенд-рендерер. - GPU (перезапуск для применения) - Выберите GPU, на котором должен работать Vita3K. - Обычная - Высокая - Точность рендеринга - Вертикальная синхронизация - Отключение вертикальной синхронизации может устранить проблемы со скоростью в некоторых играх. -Рекомендуется держать ее включенной, чтобы избежать разрывов изображения(tearing). - Отключить синхронизацию поверхностей - Скоростной хак, установите флажок, чтобы отключить синхронизацию поверхностей между CPU и GPU. -Синхронизация поверхностей необходима некоторым играм. -При отключении дает большой прирост производительности (в частности, когда включено масштабирование). - Асинхронная компиляция конвейеров - Позволяет компилировать конвейеры одновременно в нескольких параллельных потоках. -Это уменьшает задержки при компиляции конвейеров ценой временных графических сбоев. - Ближайший - Билинейный - Бикубический - Экранный фильтр - Укажите применяемый фильтр постобработки. - Внутреннее увеличение разрешения - Включить увеличение разрешения для Vita3K. -Экспериментально: не гарантируется, что игры будут правильно отображаться при увеличении отличном от 1. - Анизотропная фильтрация - Анизотропная фильтрация - это техника, позволяющая улучшить качество изображения поверхностей. -которые расположены под наклоном относительно зрителя. -Она не имеет недостатков, но может повлиять на производительность. - Замена текстур - Экспорт текстур - Импорт текстур - Формат экспорта текстур - Шейдеры - Использовать кэш шейдеров - Установите флажок, чтобы включить предварительную компиляцию кэша шейдеров при запуске игры. -Снимите флажок, чтобы отключить эту функцию. - Использовать шейдеры Spir-V (устарело) - Передавать сгенерированные шейдеры Spir-V непосредственно в драйвер. -Обратите внимание, что некоторые полезные расширения будут отключены, -и не все графические процессоры совместимы с этим режимом. - Очистить кэш и лог шейдеров - FPS хак - Игровой хак, который позволяет некоторым играм, работающим в 30 FPS, работать в 60 FPS на эмуляторе. -Обратите внимание, что это хак, и он будет работать только в некоторых играх. -На другие игры он может не повлиять или заставить их работать в два раза быстрее. - - - - Передняя камера - Задняя камера - Изображение не установлено - Установить изображение - Сплошной цвет - Статичное изображение - - - Назначение кнопки "Ввод". -Выберите свою кнопку "Ввод". - Это кнопка, которая используется в качестве "Подтвердить" в диалоговых окнах приложений. -Некоторые приложения не используют эту настройку и используют кнопку подтверждения по умолчанию. - Кружок - Крестик - Режим PS TV - Установите флажок, чтобы включить режим эмуляции PS TV. - Режим витрины - Установите флажок, чтобы включить режим витрины. - Демонстрационный режим - Установите флажок, чтобы включить демонстрационный режим. - - - Загружать приложения в полноэкранном режиме - Трассировка - Информационные - Предупреждения - Ошибки - Критические - Отключено - Уровень логирования - Выберите желаемый уровень логирования. - Архивировать логи - Установите флажок, чтобы архивировать логи. - Включает статус активность в Discord (Rich Presence), чтобы показать в Discord, какое приложение вы запускаете. - Кэш текстур - Установите флажок, чтобы включить кэширование текстур. - Показывать "шейдеров скомпилировано" - Установите флажок, чтобы отключить отображение окна "шейдеров/конвейеров скомпилировано". - Показать курсор тачпада - Установите флажок, чтобы включить отображение курсора сенсорной панели на экране. - Логировать предупреждения о совместимости - Установите флажок, чтобы включить логирование предупреждений о совместимости в GitHub Issues. - Проверять наличие обновлений - Автоматически проверять обновления при запуске. - Оверлей производительности - Отображение информации о производительности в отдельном окне на экране. - Минимальная - Низкая - Средняя - Максимальная - Детализация - Выберите желаемую детализацию оверлея производительности. - Сверху слева - Сверху по центру - Сверху справа - Внизу слева - Внизу по центру - Внизу справа - Положение - Выберите желаемое положение оверлея производительности. - Установите флажок, чтобы включить поиск путей без учета регистра на файловых системах, чувствительных к регистру. -СБРАСЫВАЕТСЯ ПРИ ПЕРЕЗАПУСКЕ - Позволяет эмулятору пытаться искать файлы независимо от регистра -на платформах, отличных от Windows. - Папка хранилища эмулируемой системы - Текущий путь эмулятора: - Изменить путь эмулятора - Изменение пути к папке эмулятора Vita3K. -Вам нужно будет вручную переместить старую папку в новое место. - Сбросить путь эмулятора - Установите путь к эмулятору Vita3K по умолчанию. -Вам нужно будет вручную переместить старую папку в новое место. - Настройки пользовательской конфигурации - Очистить пользовательскую конфигурацию - - - Отображать интерфейс - Установите флажок, чтобы показывать графический интерфейс после запуска приложения. - Отображать информационную панель - Установите флажок, чтобы показывать информационную панель внутри селектора приложений. - Язык интерфейса - Выберите язык пользовательского интерфейса. - Отображать информационные сообщения - Снимите флажок, чтобы отображать информационные сообщение только в логе. - Отображать системные приложения - Снимите флажок, чтобы отключить отображение системных приложений на главном экране. -Они будут отображаться только в главном меню. - Экран Live Area - Установите флажок, чтобы при щелчке на приложении по умолчанию открывалась Live Area. -Если флажок не установлен, щелкните приложение правой кнопкой мыши, чтобы открыть Live Area. - Растянуть область отображения - Установите флажок, чтобы увеличивать область отображения в соответствии с размером экрана. - Режим сетки - Установите флажок, чтобы перевести список приложений в режим сетки. - Размер значков приложений - Выберите размер значков приложений. - Поддержка шрифтов - Азиатский регион - Установите этот флажок, чтобы включить поддержку шрифтов для китайского и корейского языков. -Включение этой опции будет использовать больше памяти и потребует перезапуска эмулятора. - Пакет шрифтов для прошивки необходим для некоторых приложений -а также для поддержки азиатских региональных шрифтов в графическом интерфейсе. -Он также обычно рекомендуется для GUI. - Тема и фон - Идентификатор содержимого текущей темы: - Сброс темы по умолчанию - Используемый фон темы - Очистка пользовательских фонов - Текущий стартовый фон: - Сбросить стартовый фон - Альфа-канал фона - Выберите желаемую прозрачность фона. -Минимальное значение - непрозрачный, максимальное - прозрачный. - Задержка для фонов - Выберите задержку (в секундах) перед сменой фона. - Задержка для стартового экрана - Выберите задержку (в секундах) перед возвратом на стартовый экран. - - - Подключен к PSN - Если флажок установлен, игры будут считать, что пользователь подключен к сети PSN (но не в сети). - IP-адрес - Маска подсети - - Сохранить - Сохранить и перезапустить - Сохранить и применить - Сохранить и закрыть - Нажмите на Сохранить, чтобы сохранить изменения. - - - - Браузер - Веб-браузер - Призы - Коллекция призов - Настройки - Управление данными - - - - Удалить призы - Информация о призах, сохранённая для этого пользователя, -будет удалена. - Не -разбл. -
Подробности
- Получены - Название - Призы отсутствуют. -Можно получать призы, используя приложение, поддерживающее функцию призов. - Не получен - Исходный - Сортировка - Призы - Категория - Выполнено - Обновлен -
- - - Выбрать пользователя - Создание пользователя - Создан следующий пользователь: - Изменить пользователя - Открыть папку пользователя - Это имя уже используется. - Удалить пользователя - Выберите пользователя, которого хотите удалить. - Выбранный пользователь был удален: - При удалении пользователя, его сохраненные данные, призы будут удалены. - Пользователь будет удален. -Продолжить? - Пользователь удален. - Выбрать аватар - Сбросить аватар - Подтвердить - Автоматический вход в систему - - - - Доступна новая версия Vita3K. - Назад - Отменить обновление? -Если отменить обновление, то при следующем его запуске Vita3K начнет загрузку с этой точки. - Идет загрузка... -После завершения загрузки Vita3K автоматически перезапустится и установит новое обновление. - Не удалось завершить обновление. - Осталось минут: {} - Далее - Осталось секунд: {} - Обновить Vita3K? - Более новая версия Vita3K уже установлена. - Последняя версия Vita3K уже установлена. - Новые функции в версии {} - Автор(ы) - Описание - Обновление - Версия {} - - - - Vita3K, эмулятор PlayStation Vita - Vita3K - это эмулятор PlayStation Vita с открытым исходным кодом, написанный на C++ для Windows, Linux, macOS и Android. - Эмулятор до сих пор находится в разработке, мы очень ценим любые отзывы и тесты. - Скачать прошивку - Скачать пакет шрифтов прошивки - Полное руководство для настройки Vita3K можно найти - здесь - - Проверьте списки совместимости, чтобы узнать, что сейчас запускается - Список совместимости коммерческих игр - Список совместимости homebrew - Помощникам всегда рады! - Дополнительную информацию можно найти на канале #help в - Vita3K не поощряет пиратство. Вы должны использовать дампы своих собственных игр. - Показать в следующий раз - -
diff --git a/lang/system/sv.xml b/lang/system/sv.xml deleted file mode 100644 index 6f2544a23..000000000 --- a/lang/system/sv.xml +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - Öppna inställningssökväg - Installera firmware - Installera .pkg - Installera .zip, .vpk - Installera licens - Avsluta - - - Senaste använda program - - - Hantering av användare - - - Tangentbordskontroller - - - Välkommen - - - - - - Handbok - Uppdatera - - - Den här applikationen och alla tillhörande data, inklusive sparade data, kommer att raderas. - - Uppdateringshistorik - Version {} - - Möjligt - Inte möjligt - Nivå {} - Namn - Vinna trophy - Föräldrakontroll - Uppdaterat - Storlek - Version - - - - - Ett fel har inträffat. -Felkod: {} - Avbryt - Stänga - Kunde inte ladda filen. - Kunde inte spara filen. - Radera - Fel - Filen är skadad. - Aktivera mikrofonen. - Nej - Vänta... - Välj alla - - Ja - - söndag - måndag - tisdag - onsdag - torsdag - fredag - lördag - - - Januari - Februari - Mars - April - Maj - Juni - Juli - Augusti - September - Oktober - November - December - - - januari - februari - mars - april - maj - juni - juli - augusti - september - oktober - november - december - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Dold trophy - 1 timme sedan - 1 minut sedan - Brons - Guld - Platina - Silver - {} timmar sedan - {} minuter sedan - - - - - De markerade applikationerna och alla tillhörande data, inklusive sparade data, kommer att raderas. - Det finns inga innehållsobjekt. - - - De sparade data som har markerats kommer att raderas. - Det finns inga sparade data. - - Tema - Ledigt utrymme - Återställ alla - - - - Num - - - - - Förbereder start av applikationen... - - - - Vill du avbryta raderingen? - Radering slutförd. - Vill du radera dessa sparade data? - - -
Detaljer
- Uppdaterat -
- - Vill du avbryta laddningen? - Det finns inga sparade data. - Laddning slutförd. - Laddar... - Vill du ladda in dessa sparade data? - - - Vill du avbryta sparningen? - Kunde inte spara filen. -Det finns inte tillräckligt med ledigt utrymme på minneskortet. Du måste skapa minst {} ledigt utrymme om du vill spara dina framsteg i den här applikationen. - -Skapa mer ledigt utrymme genom att trycka på PS-knappen för att pausa den här applikationen och radera sedan övriga applikationer eller data. - Det finns inte tillräckligt med ledigt utrymme på minneskortet. -Du måste skapa minst {} ledigt utrymme om du vill fortsätta använda applikationen. - -Tryck på PS-knappen för att pausa den här applikationen och radera sedan övriga applikationer eller innehåll. - Nya sparade data - Sparning slutförd. - Vill du spara datan? - Sparar... - Sparar... -Stäng inte av systemet och stäng inte applikationen. - Vill du skriva över dessa sparade data? - -
-
- - - Följande applikation stängs. - Data att radera: - - - - Applikationen har lagts till på hemskärmen. - Radera alla - Notiserna raderas. - Kunde inte installera. - Installation slutförd. - Installerar... - Det finns inga notiser. - Du har vunnit en trophy! - - - - Tillbaka - Du har slutfört den första inställningen. -Ditt Vita3K-system är redo! - Välj ett språk. - Vita3K kräver fullständig filåtkomst. -Tryck på 'Ge Åtkomst' för att fortsätta. - Ge Åtkomst - Installera firmware. - Nästa - - - - Starta - Fortsätt - - - - - Standardinställning - - - Namn - Leverantör - Uppdaterat - Storlek - Version - - Det här temat kommer att raderas. - - - Bild - Lägg till bild - - - - - - - ÅÅÅÅ/MM/DD - DD/MM/ÅÅÅÅ - MM/DD/ÅÅÅÅ - - - 12-timmarsklocka - 24-timmarsklocka - - - - Systemspråk - - - - Danska - Tyska - Engelska (Storbritannien) - Engelsk (USA) - Spanska - Franska - Italienska - Holländska - Norska - Polska - Portugisiska (Brasilien) - Portugisiska (Portugal) - Ryska - Finska - Svenska - Turkiska - - - - - - - - - - - IP-adress - Undernätmask - - Spara & Stänga - - - - Webbläsare - Webbläsare - Trophies - Trophy-samling - Inställningar - Innehållshanterare - - - -
Detaljer
- Vunna - Namn - Det finns inga trophies. -Du kan få trophies om du använder en applikation som stöder trophy-funktionen. - Ej vunnen - Ursprunglig - Sortera - Trophies - Grad - Framsteg - Uppdaterat -
- - - Välj användare - Skapa användare - Följande användare har skapats: - Namnet finns redan. - Ta bort användare - Välj den användare du vill ta bort. - Följande användare kommer att tas bort: - Om du tar bort användaren tas de data, troféer som användaren sparat bort. - Användare kommer att tas bort. -Är du säker på att du vill fortsetta? - Användaren har tagits bort. - Välj avatar - Bekräfta - - - - En ny version av Vita3K är tillgänglig. - Tillbaka - Vill du avbryta uppdateringen? -Om du avbryter kommer Vita3K att börja ladda ned från den här punkten nästa gång du uppdaterar. - Laddar ned... -När nedladdningen är slutförd kommer Vita3K automatiskt att startas om och den nya uppdatera installeras. - Kunde inte slutföra uppdateringen. - {} minuter kvar - Nästa - {} sekunder kvar - Vill du uppdatera Vita3K? - Den senare versionen av Vita3K har redan installerats. - Den senaste versionen av Vita3K har redan installerats. - Nya funktioner i version {} - Uppdatera - Version {} - -
diff --git a/lang/system/tr.xml b/lang/system/tr.xml deleted file mode 100644 index 75c7d9fb2..000000000 --- a/lang/system/tr.xml +++ /dev/null @@ -1,409 +0,0 @@ - - - - - - Tercih Klasörünü aç - Yazılım Yükle - .pkg Yükle - .zip, .vpk Yükle - Lisans Yükle - Çıkış - - - Son Kullanma - - - Kullanıcı Yönetimi - - - Klavye Kontrol - - - Hoşgeldin - - - - - - - - - Kılavuzu - Güncelle - - - Bu uygulama ve kayıtlı veriler dahil ilgili tüm veriler silinecek. - - Güncelleme Geçmişi - Sürüm {} - - Uygun - Uygun değil - Seviye {} - Ad - Kupa Kazanma - Ebeveyn Kontrolleri - üncelleme Tarihi - Boyut - Sürüm - Son kullanma - Süre kullanıldı - Asla - - - - - Bir hata oluştu. -Hata Kodu: {} - İptal - Kapat - Dosya yüklenemedi. - Dosya kaydedilemedi. - Sil - Hata - Dosya bozuk. - Hayır - Tamam - Lütfen bekleyin... - Tümünü Seç - - Evet - - Pazar - Pazartesi - Salı - Çarşamba - Perşembe - Cuma - Cumartesi - - - Ocak - Şubat - Mart - Nisan - Mayıs - Haziran - Temmuz - Ağustos - Eylül - Ekim - Kasım - Aralık - - - Ocak - Şubat - Mart - Nisan - Mayıs - Haziran - Temmuz - Ağustos - Eylül - Ekim - Kasım - Aralık - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Gizli Kupa - 1 Saat Önce - 1 Dakika Önce - Bronz - Altın - Platin - Gümüş - {} Saat Önce - {} Dakika Önce - - - - - Seçilen uygulamalar ve kayıtlı veriler dahil ilgili tüm veriler silinecek. - İçerik öğesi yok. - - - Seçilen kayıtlı veri öğeleri silinecek. - Kayıtlı Veri yok. - - Tema - Boş Alan - Tümünü Temizle - - - - Num - - - - - Uygulama başlatma için hazırlanıyor... - - - - Silmeyi iptal etmek istiyor musunuz? - Silme tamamlandı. - Bu kayıtlı verileri silmek istiyor musunuz? - - -
Ayrıntılı Bilgi
- Güncelleme Tarihi -
- - Yüklemeyi iptal etmek istiyor musunuz? - Kayıtlı veri yok. - Yükleme tamamlandı. - Yükleniyor... - Bu kayıtlı verileri yüklemek istiyor musunuz? - - - Kaydetmeyi iptal etmek istiyor musunuz? - Dosya kaydedilemedi. -Hafıza kartında yeterli boş alan yok. Uygulamadaki ilerlemenizi kaydetmek için en az {} daha boş alan oluşturmanız gerekiyor. - -Boş alan oluşturmak için PS düğmesine basarak uygulamayı duraklatın ve ardından başka uygulamaları ya da içerikleri silin. - Hafıza kartında yeterli boş alan yok. -Uygulamayı kullanmaya devam etmek için en az {} daha boş alan oluşturmanız gerekiyor. - -Uygulamayı duraklatmak için PS düğmesine basın ve ardından başka uygulamaları ya da içerikleri silin. - Yeni Kaydedilmiş Veriler - Kaydetme tamamlandı. - Verileri kaydetmek istiyor musunuz? - Kaydediliyor... - Kaydediliyor... -Sistemi veya uygulamayı kapatmayın. - Bu kayıtlı verilerin üzerine yazmak istiyor musunuz? - -
-
- - - Şu uygulama kapatılacak. - Silinecek Veriler: - - - - Uygulama, giriş ekranına eklendi. - Tümünü Sil - Bildirimler silinecek. - Kurulamadı. - Kurulum tamamlandı. - Kuruluyor... - Bildirim yok. - Kupa kazandınız! - - - - Geri - Başlangıç ayarlarını tamamladınız. -Vita3K sisteminiz artık hazır! - Dil seçin. - Vita3K tam dosya erişimi gerektirir. -Devam etmek için 'Erişim Ver' düğmesine dokunun. - Erişim Ver - Yazılım Yükle. - İleri - - - - Başlat - Devam - - - - - Varsayılan - - - Ad - Sağlayıcı - Güncelleme Tarihi - Boyut - Sürüm - - Bu tema silinecek. - - - Görüntü - - - - - - - YYYY/AA/GG - GG/AA/YYYY - AA/GG/YYYY - - - 12 Saatlik Zaman Formatı - 24 Saatlik Zaman Formatı - - - - Sistem Dili - - - - Danca - Almanca - İngilizce (Birleşik Krallık) - İngilizce (Amerika Birleşik Devletleri) - İspanyolca - Fransızca - İtalyanca - Flemenkçe - Norveççe - Lehçe - Portekizce (Brezilya) - Portekizce (Portekiz) - Rusça - Fince - İsveççe - Türkçe - - - - - - - - - - - IP Adresi - Alt Ağ Maskesi - - Kaydet & Kapat - - - - Tarayıcı - İnternet Tarayıcısı - Kupalar - Kupa Koleksiyonu - Ayarlar - İçerik Yöneticisi - - - -
Ayrıntılı Bilgi
- Kazanıldığı Tarih - Ad - Kupa yok. -Kupa özelliğini destekleyen bir uygulamayı kullanarak kupa kazanabilirsiniz. - Kazanılmadı - Orijinal - Sırala - Kupalar - Derece - İlerleme - Güncelleme Tarihi -
- - - Kullanıcı Seç - Kullanıcı Oluştur - Şu kullanıcı oluşturuldu: - Kullanıcı Düzenle - Bu isim zaten kullanılıyor. - Kullanıcı Sil - Silmek istediğiniz kullanıcıyı seçin. - Şu kullanıcı silinecek: - Kullanıcıyı silerseniz, bu kullanıcının kayıtlı verileri, kupalar silinir. - Kullanıcı silinecek. -Devam etmek istediğinizden emin misiniz? - Kullanıcı Silindi. - Avatar Seç - Avatar Sıfırla - Onayla - Otomatik Kullanıcı Girişi - - - - Vita3K yeni bir sürümü mevcut. - Geri - Güncelleme işlemini iptal etmek istiyor musunuz? -İptal ederseniz, bir sonraki güncellemenizde Vita3K indirmeye kaldığı yerden devam eder. - İndiriliyor... -İndirme tamamlandığında, Vita3K otomatik olarak yeniden başlatılır ve yeni güncelleme kurulur. - Güncelleme tamamlanamadı. - {} Dakika Kaldı - İleri - {} Saniye Kaldı - Vita3K güncellemek istiyor musunuz? - Vita3K sonraki sürümü önceden kuruldu. - Vita3K en son sürümü önceden kuruldu. - {} Sürümündeki Yeni Özellikler - Güncelle - Sürüm {} - -
diff --git a/lang/system/zh-s.xml b/lang/system/zh-s.xml deleted file mode 100644 index 99f3c556e..000000000 --- a/lang/system/zh-s.xml +++ /dev/null @@ -1,990 +0,0 @@ - - - - - - 打开存放路径 - 打开补丁路径 - 打开纹理路径 - 安装固件 - 安装pkg - 安装zip、vpk - 安装授权 - 退出 - - - 最近使用的应用程序 - - - - 线程 - 信号量 - 互斥锁 - 轻量互斥锁 - 条件变量 - 轻量条件变量 - 事件标志 - 内存分配 - 反汇编 - - - 用户管理 - - - 键盘控制 - - - 欢迎 - - - - - Vita3K:PS Vita/PS TV模拟器,是全球首个实用的PS Vita/PS TV模拟器。 - Vita3K是使用C++编写的实验性开源PlayStation Vita/PlayStation TV模拟器,适用于Windows、Linux、macOS以及Android操作系统。 - 特别鸣谢:Vita3K图标设计者: - 如果您有兴趣参与贡献,请浏览我们的: - 更多信息请访问我们的网站: - 如果您想支持我们,您可以捐赠或者订阅我们的: - Vita3K人员名单 - 开发人员 - 贡献者 - 赞助者 - - - - - 检查应用程序状态 - 复制Vita3K摘要 - 打开状态报告 - 创建状态报告 - 更新数据库 - - - 名称和标题ID - 应用程序摘要 - - 创建快捷方式 - - 创建 - 编辑 - 移除 - - - 打开文件夹 - 补丁 - 追加内容 - 授权 - 着色器缓存 - 着色器日志 - - - 说明书 - 升级 - - - 删除此应用程序及所有相关数据,包括保存数据。 - 删除应用程序可能需要一段时间, -取决于它的大小和您的硬件。 - 要删除此补丁吗? - 要删除此追加内容吗? - 要删除此授权吗? - - - 重置最近游玩时间 - - 更新历史记录 - {}版本 - - 有权 - 无权 - 级别 {} - 名称 - 获得奖杯 - 视听者限制 - 升级日期 - 大小 - 版本 - 标题ID - 类别 - 数字版游戏 - 游戏补丁 - 最近使用时间 - 使用时间 - 从来没有 - - - {}秒 - {}分钟{}秒 - {}小时{}分钟{}秒 - {}天{}小时{}分钟{}秒 - {}周{}天{}小时{}分钟{}秒 - - - - - 发生了错误。 -错误代码:{} - 取消 - 关闭 - 无法读取文件。 - 无法保存文件。 - 删除 - 错误 - 文件已损坏。 - 请启用麦克风。 - - 请稍等… - 搜索 - 全部选择 - - 提交 - - - 星期日 - 星期一 - 星期二 - 星期三 - 星期四 - 星期五 - 星期六 - - - 1月 - 2月 - 3月 - 4月 - 5月 - 6月 - 7月 - 8月 - 9月 - 10月 - 11月 - 12月 - - - 1月 - 2月 - 3月 - 4月 - 5月 - 6月 - 7月 - 8月 - 9月 - 10月 - 11月 - 12月 - - - - 1日 - 2日 - 3日 - 4日 - 5日 - 6日 - 7日 - 8日 - 9日 - 10日 - 11日 - 12日 - 13日 - 14日 - 15日 - 16日 - 17日 - 18日 - 19日 - 20日 - 21日 - 22日 - 23日 - 24日 - 25日 - 26日 - 27日 - 28日 - 29日 - 30日 - 31日 - - - - 1日 - 2日 - 3日 - 4日 - 5日 - 6日 - 7日 - 8日 - 9日 - 10日 - 11日 - 12日 - 13日 - 14日 - 15日 - 16日 - 17日 - 18日 - 19日 - 20日 - 21日 - 22日 - 23日 - 24日 - 25日 - 26日 - 27日 - 28日 - 29日 - 30日 - 31日 - - 隐藏奖杯 - 1小时前 - 1分钟前 - Bronze(铜) - Gold(金) - Platinum(白金) - Silver(银) - {}小时前 - {}分钟前 - - - - - 未知 - 无法启动 - 可引导 - 有图像 - 可进入菜单 - 可进入游戏- - 可进入游戏+ - 可玩 - - - - - 无法获取当前兼容性数据库,请检查防火墙/互联网连接后再重试。 - 无法下载更新于{}的应用程序兼容性数据库,请稍后重试。 - 无法读取已下载更新于{}的应用程序兼容性数据库。 - 兼容性数据库已成功从{}更新至{}。 - -已列出{}个新的应用程序,使总数达到{}个! - 兼容性数据库已成功从{}更新至{}。 - -已列出{}个应用程序! - 已成功下载并加载{}更新的兼容性数据库。 - -已列出{}个应用程序! - - - - 正在编译着色器 - 已编译{}个管线 - 已编译{}个着色器 - - - - - 即将删除选择的应用程序及所有相关数据,包括保存数据。 - 没有内容。 - - - 所选择的保存数据将被删除。 - 保存数据不存在。 - - 主题 - 空余容量 - 全部清除 - - - - 已连接{}个控制器 - 编号 - 体感支持 - 重新绑定控制 - LED灯色 - 使用自定义颜色 - 勾选此项可为控制器的LED灯使用自定义颜色。 - - 绿 - - 未连接兼容的控制器。 -请连接与SDL3兼容的控制器。 - 禁用体感 - 使用内置设备传感器 - 重置控制器绑定 - - - - - 映射按键 - 左摇杆上 - 左摇杆下 - 左摇杆右 - 左摇杆左 - 右摇杆上 - 右摇杆下 - 右摇杆右 - 右摇杆左 - 方向键上 - 方向键下 - 方向键右 - 方向键左 - 方形键 - 交叉键 - 圆形键 - 三角键 - Start键 - Select键 - PS键 - L1键 - R1键 - 仅在PS TV模式下有效。 - L2键 - R2键 - L3键 - R3键 - 图形用户界面 - 全屏 - 切换触摸 - 在背触和前触之间切换。 - 切换图形用户界面可见性 - 在应用程序运行时,在屏幕顶部显示和隐藏图形用户界面之间切换。 - 杂项 - 切换纹理替换 - 截图 - 捏合变更键 - 交替捏合向内键 - 交替捏合向外/伸缩键 - 该按键已被使用并绑定其他键或被预留。 - 重置控制绑定 - - - - - 正在准备启动应用程序… - - - - 要取消删除吗? - 删除完毕。 - 要删除此保存数据吗? - - -
详细信息
- 升级日期 -
- - 要取消读取吗? - 保存数据不存在。 - 读取完毕。 - 正在读取… - 要读取此保存数据吗? - - - 要取消保存吗? - 无法保存文件。 -存储卡没有足够的空余容量。若要保存应用程序的进度,需准备{}以上的空余容量。 - -若要准备空余容量,请按下PS键暂停此应用程序,并删除其它应用程序或内容。 - 存储卡没有足够的空余容量。 -若要继续执行应用程序,请准备{}以上的空余容量。 - -请按下PS键暂停此应用程序,并删除其它应用程序或内容。 - 新建保存数据 - 保存完毕。 - 要保存数据吗? - 正在保存… - 正在保存… -请勿关闭电源或关闭应用程序。 - 要覆盖此保存数据吗? - -
-
- - - 下列应用程序即将关闭。 - 删除的数据: - - - - 筛选 - 应用程序排序按 - 全部 - 按地区 - 美版 - 欧版 - 日版 - 亚版 - 按类型 - 商业 - 自制 - 按兼容性状态 - 版本 - 类别 - 兼容性 - 最近使用时间 - 刷新 - - - - 已将应用程序添加至主画面。 - 全部删除 - 最新资讯将被删除。 - 无法安装。 - 安装完毕。 - 正在安装… - 没有最新资讯。 - 已获得奖杯! - - - - 返回 - 初始设定已完成。 -请尽情享受Vita3K吧。 - 请选择语言。 - 请选择存放路径。 - Vita3K需要完整的文件访问权限。 -点击"授予访问权限"以继续。 - 授予访问权限 - 当前模拟器路径 - 更改模拟器路径 - 重置模拟器路径 - 安装固件。 - 强烈建议安装全部固件文件。 - 已安装: - 下载字体包 - 安装固件文件 - 请选择界面设定。 - 信息栏可见 - 勾选此项在应用程序选择页面内显示信息栏。 -信息栏是时间、电量以及信息中心。 - Live Area应用程序屏幕 - 勾选此项在点击应用程序时默认打开Live Area页面。 -如果禁用,请右键点击应用程序将其打开。 - 网格模式 - 勾选此项将应用程序列表设置为像PS Vita一样的网格模式。 - 应用程序图标大小 - 选择您喜欢的图标大小。 - 已完成。 - 继续 - - - - - 固件安装 - 正在安装,请稍等… - 固件已成功安装。 - 固件版本: - 不存在固件字体包,请下载并安装。 - 一些应用程序需要固件字体包,也需要亚洲区域字体支持。(强烈建议安装) - 删除固件安装文件? - - - 请选择授权类型 - 选择work.bin/rif - 输入zRIF - 输入zRIF密钥 - 请在此处输入您的zRIF - Ctrl+C复制,Ctrl+V粘贴。 - 删除pkg文件? - 删除work.bin/rif文件? - 无法安装该包。 -请检查pkg以及work.bin/rif文件或zRIF密钥。 - - - 请选择安装类型 - 选择文件 - 选择目录 - 发现兼容内容的{}个压缩包。 - 已成功安装{}个压缩包中的内容: - 更新应用程序至: - 无法安装{}个压缩包中的内容: - 未发现兼容内容的{}个压缩包: - 删除压缩包? - - - 已成功安装授权。 - 无法安装授权。 -请检查work.bin/rif文件或zRIF密钥。 - - - 是否重新安装该内容? - 已经安装过该内容。 - 您想要重新安装并覆盖已存在的数据吗? - - - - - 开始 - 继续 - - 在控制设置中使用键盘配置 - 未检测到固件,强烈建议安装 - 未检测到固件字体,建议在“Live Area”中安装字体固件 - Live Area帮助 - 浏览应用程序列表 - 方向键、左摇杆、滚轮上/下或使用滚动条 - 开启应用 - 点击开始或按交叉键 - 应用程序运行时显示/隐藏 - 按PS键 - 退出Live Area - 点击Esc或按圆形键 - 说明书帮助 - 浏览页面 - 方向键、左摇杆、滚轮上/下或使用滚动条、点击</> - 隐藏/显示按钮 - 点击左键或按三角键 - 退出说明书 - 点击Esc或按PS键 - - - - - 无法读取“{}”, -请检查vita3k.log查看控制台输出的详细信息。 -1、您是否安装了固件? -2、重新提取您已拥有的PS Vita应用程序/游戏,并在Vita3K中安装。 -3、如果您想要安装/运行Vitamin是不支持的。 - - - - 手柄虚拟按键 - 在游戏中显示手柄虚拟按键 - 隐藏手柄虚拟按键 - 修改手柄虚拟按键 - 虚拟按键缩放 - 虚拟按键不透明度 - 重置手柄布局 - 显示前/后触屏切换按键。 - 仅当启用PSTV模式时,才会显示L2/R2键。 - - - - 平均 - 最低 - 最高 - - - - - 原始 - - 寻找PSVita自制主题 - - 名称 - 提供者 - 升级日期 - 大小 - 版本 - 内容ID - - 删除此主题。 - - - 图像 - 添加图像 - - - 删除背景 - 添加背景 - - - - - 年/月/日 - 日/月/年 - 月/日/年 - - - 12小时制 - 24小时制 - - - - 系统语言 - - - - 丹麦语 - 德语 - 英语(英国) - 英语(美国) - 西班牙语 - 法语 - 意大利语 - 荷兰语 - 挪威语 - 波兰语 - 葡萄牙语(巴西) - 葡萄牙语(葡萄牙) - 俄语 - 芬兰语 - 瑞典语 - 土耳其语 - - - - - - - - - 模块模式 - 自动 - 选择自动模式会使用预设的模块列表。 - 自动&手动 - 选择此模式会加载预设模块以及从列表中所选定的模块。 - 手动 - 选择手动模式会从列表中加载所选定的模块。 - 模块列表 - 选择您所需的模块。 - 搜索模块 - 清除列表 - 没有模块存在。 -请下载并安装最新的PS Vita固件。 - 刷新列表 - - - 启用优化 - 勾选此项启用额外的CPU JIT优化。 - - - 重置 - 后端渲染器 - 选择您喜欢的后端渲染器。 - GPU(重启后应用) - 选择Vita3K应使用的GPU。 - 添加自定义驱动 - 下载自定义驱动 - 移除自定义驱动 - 标准 - - 渲染精度 - 垂直同步 - 禁用垂直同步可以解决某些游戏的速度问题。 -建议保留启用它以避免画面撕裂。 - 禁用表面同步 - 提升速度,勾选此项禁用处理器与显卡之间的表面同步。 -某些游戏需要表面同步, -如果禁用(特别是在放大时),则可大幅提升性能。 - 异步管线编译 - 允许在多个并发线程上同时编译管线。 -这可以减少管线编译卡顿,但代价是会出现短暂的图形故障。 - 最邻近 - 双线性 - 双三次 - 屏幕过滤 - 设置要应用的后处理过滤器。 - 内部分辨率放大 - 为Vita3K启用放大效果。 -实验性:不保证游戏能在超过1倍时正确渲染。 - 各向异性过滤 - 各向异性过滤是一种提高表面图像质量的技术, -它们相对于观察者是倾斜的, -它没有缺点,但会影响性能。 - 纹理替换 - 导出纹理 - 导入纹理 - 纹理导出格式 - FPS修改 - 游戏修改允许一些以30FPS运行的游戏在模拟器上以60FPS的速度运行。 -请注意这只是一个修改,仅适用于某些游戏。 -在其他游戏中,它可能没有效果或使其出现两倍速。 - 禁用 - 双重缓冲 - 外部主机 - 页表 - 原生缓冲 - 内存映射方式 - 内存映射提高了性能,减少了内存使用量,并修复了许多图形问题。 -但是它在某些GPU上可能不稳定。 - 启用睿频模式 - 提供了一种方法来强制GPU以允许的最高时钟频率运行(温控仍然适用)。 - 着色器 - 使用着色器缓存 - 勾选此项以启用着色器缓存以在游戏启动时对其进行预编译。 -取消勾选则禁用该功能。 - 使用Spir-V着色器(已弃用) - 将生成的Spir-V着色器直接传递给驱动程序。 -请注意,一些有益的扩展功能将被禁用, -并非所有显卡都与此兼容。 - 清除着色器缓存以及日志 - - - - 确认按键分配 -请选择您的“确认”按键。 - 这是在应用程序对话框中用作“确认”的按键。 -某些应用程序不使用此功能并获取默认确认按键。 - - × - PS TV模式 - 勾选此项启用PS TV模拟模式。 - Show模式 - 勾选此项启用Show模式。 - Demo模式 - 勾选此项启用Demo模式。 - - - 以全屏启动应用程序 - 追踪 - 信息 - 警告 - 错误 - 严重 - 关闭 - 日志级别 - 选择您喜欢的日志级别。 - 归档日志 - 勾选此项启用归档日志。 - 启用Discord Rich Presence以在Discord上展示您正在运行的应用程序。 - 纹理缓存 - 取消勾选此项禁用纹理缓存。 - 显示编译着色器 - 取消勾选此项禁用编译着色器对话框的显示。 - 显示触摸板光标 - 取消勾选此项禁用屏幕上的触摸板光标的显示。 - 记录兼容性警告 - 勾选此项启用对GitHub议题的兼容性警告的记录。 - 检查更新 - 启动时自动检查更新。 - 文件加载延迟 - 文件加载延迟(毫秒)。 -某些游戏需要设置此项,以防加载速度比真实硬件快得多(例如:寂静岭)。 -默认为0毫秒。 - 性能图层 - 在屏幕上以图层的形式显示性能信息。 - 最少 - - 中等 - 最多 - 细节 - 选择您喜欢的性能图层细节。 - 左上 - 顶部中间 - 右上 - 左下 - 底部中间 - 右下 - 位置 - 选择您喜欢的性能图层位置。 - 勾选以在区分大小写的文件系统上启用不区分大小写的路径查找。 -重启时复位 - 允许模拟器尝试在非Windows平台上不区分大小写的文件搜索。 - 模拟系统存储文件夹 - 当前模拟器路径: - 更改模拟器路径 - 更改Vita3K模拟器文件夹路径。 -您需要手动将旧文件夹移动到新位置。 - 重置模拟器路径 - 重置Vita3K模拟器为默认路径。 -您需要手动将旧文件夹移动到新位置。 - 自定义配置设置 - 清除自定义配置 - 截图图像类型 - - 截图格式 - - - 图形用户界面可见 - 勾选此项在启动应用程序后显示图形用户界面。 - 信息栏可见 - 勾选此项在应用程序选择页面内显示信息栏。 - 图形用户界面语言 - 选择您的用户语言。 - 显示信息消息 - 取消勾选此项只在日志中显示信息消息。 - 显示系统应用程序 - 取消勾选此项禁用从主画面中显示的系统应用程序。 -它们将仅显示在主菜单栏中。 - Live Area应用程序屏幕 - 勾选此项在点击应用程序时默认打开Live Area页面。 -如果禁用,请右键点击应用程序将其打开。 - 拉伸显示区域 - 勾选此项扩大显示区域以适应屏幕尺寸。 - 全屏高清精准渲染 - 对于屏幕比例为16:9的高清显示器,勾选此项可在全屏模式下获得像素级精准渲染, -但代价是屏幕顶部和底部会有轻微裁剪。 - 网格模式 - 勾选此项将应用程序列表设置为网格模式。 - 应用程序图标大小 - 选择您喜欢的图标大小。 - 字体支持 - 亚洲区域 - 勾选此项启用对中文以及韩文的字体支持。 -启用此功能将占用更多内存,并且需要您重启模拟器才能生效。 - 一些应用程序需要固件字体包 -以及在图形用户界面中的亚洲区域字体支持。 -通常也强烈建议将其用于图形用户界面。 - 主题&背景 - 当前主题内容ID: - 重置原始主题 - 使用主题背景 - 清除用户背景 - 当前主画面的背景: - 重置主画面的背景 - 背景透明度 - 选择您喜欢的背景透明度。 -最小值不透明,最大值透明。 - 背景延迟 - 选择更改背景前的延迟(以秒为单位)。 - 开始画面延迟 - 选择返回开始画面前的延迟(以秒为单位)。 - - - 已登入PSN - 如果勾选此项,游戏将认为用户已连接至PSN(但为离线)。 - IP地址 - 选择在adhoc中使用的IP地址。 - 子网掩码 - 启用HTTP - 勾选此项启用游戏在互联网上使用HTTP协议。 - HTTP超时尝试 - 当服务器没有响应时要进行多次尝试。 -如果您有非常不稳定或是非常慢的网络可能会很有用。 - HTTP超时休眠 - 当服务器没有响应时尝试进入休眠时间。 -如果您有非常不稳定或是非常慢的网络可能会很有用。 - HTTP读取终止尝试 - 当没有更多数据可读取时,尝试多次可以提升性能, -但如果您的网络很糟糕,则会导致游戏不稳定。 - HTTP读取终止休眠 - 尝试在没有更多数据可读取时进入休眠时间,这可以提升性能, -但如果您的网络很糟糕,则会导致游戏不稳定。 - - - 导入记录 - 记录模块导入符号。 - 导出记录 - 记录模块导出符号。 - 着色器记录 - 记录在每次绘制调用中使用的着色器。 - 统一变量记录 - 记录着色器统一变量的名称和值。 - 保存颜色表面 - 保存颜色表面到文件。 - ELF提取 - 将加载的代码提取为ELF。 - 验证层(要求重启) - 启用Vulkan验证层。 - 取消监视代码 - 监视代码 - 取消监视内存 - 监视内存 - 取消监视导入调用 - 监视导入调用 - - 保存并重启 - 保存并应用 - 保存并关闭 - 点击保存保留您的更改。 - - - - 浏览器 - 互联网浏览器 - Trophy(奖杯) - Trophy(奖杯)收藏 - 设定 - 内容管理 - - - - 该用户已保存的全部奖杯信息将被删除。 - 删除奖杯 - 该用户已保存的奖杯信息将被删除。 - 未解锁 -
详细信息
- 获得日期 - 名称 - 找不到奖杯。 -您可通过游玩支持奖杯功能的应用程序获得奖杯。 - 未获得 - 原始 - 排列 - 奖杯数 - Grade(等级) - 达成率 - 升级日期 - 高级 - 展示隐藏奖杯 -
- - - 选择用户 - 创建用户 - 已创建以下用户。 - 编辑用户 - 打开用户文件夹 - 此名称已被使用。 - 删除用户 - 请选择要删除的用户。 - 将删除以下用户。 - 若删除用户,该用户的保存数据、奖杯也会一并被删除。 - 用户将被删除。 -确定要继续吗? - 用户已删除。 - 选择虚拟形象 - 重置虚拟形象 - 确定 - 自动登录用户 - - - - 检测到新版本的Vita3K。 - 返回 - 要取消升级吗? -若取消升级,下次升级时将从中断处开始下载。 - 下载中… -下载完毕后,Vita3K将会自动重新启动,并安装新的升级。 - 无法完成升级。 - 剩余 {}分钟 - 继续 - 剩余 {}秒 - 要升级Vita3K吗? - 已安装新版本的Vita3K。 - 已安装最新版本的Vita3K。 - {}版本的追加功能 - 作者 - 注释 - 更新 - {}版本 - - - - Vita3K PlayStation Vita模拟器 - Vita3K是使用C++编写的开源PlayStation Vita模拟器,适用于Windows、Linux、macOS以及Android。 - 目前模拟器仍处于开发阶段,因此非常感谢任何反馈和测试。 - 要开始使用,请安装全部PS Vita固件文件。 - 下载预装固件 - 下载固件 - 下载固件字体包 - 有关如何设置Vita3K的综合指南可在 - 快速入门 - 页面中找到。 - 参考商业游戏以及自制程序兼容性列表来查看目前可运行的内容。 - 商业游戏兼容性列表 - 自制程序兼容性列表 - 欢迎参与贡献! - 可以在#help频道中找到更多支持 - Vita3K不纵容盗版。您必须提取您自己拥有的游戏。 - 下次显示 - -
diff --git a/lang/system/zh-t.xml b/lang/system/zh-t.xml deleted file mode 100644 index d13935c73..000000000 --- a/lang/system/zh-t.xml +++ /dev/null @@ -1,990 +0,0 @@ - - - - - - 開啟存放路徑 - 開啟補丁路徑 - 開啟紋理路徑 - 安裝韌體 - 安裝pkg - 安裝zip、vpk - 安裝授權 - 離開 - - - 最近使用的應用程式 - - - - 執行緒 - 號誌 - 互斥鎖 - 輕量互斥鎖 - 條件變數 - 輕量條件變數 - 事件旗標 - 記憶體分配 - 反組譯 - - - 使用者管理 - - - 鍵盤控制 - - - 歡迎 - - - - - Vita3K:PS Vita/PS TV模擬器,是全球首個實用的PS Vita/PS TV模擬器。 - Vita3K是使用C++編寫的實驗性開源PlayStation Vita/PlayStation TV模擬器,適用於Windows、Linux、macOS以及Android作業系統。 - 特別鳴謝:Vita3K圖標設計者: - 如果您有興趣參與貢獻,請瀏覽我們的: - 更多資訊請造訪我們的網站: - 如果您想支援我們,您可以捐贈或者訂閱我們的: - Vita3K人員名單 - 開發者 - 參與者 - 贊助者 - - - - - 檢查應用程式狀態 - 複製Vita3K摘要 - 開啟狀態匯報 - 建立狀態匯報 - 更新資料庫 - - - 名稱和標題ID - 應用程式摘要 - - 建立快取方式 - - 建立 - 編輯 - 移除 - - - 開啟資料夾 - 補丁 - 追加內容 - 授權 - 著色器快取 - 著色器記錄 - - - 說明書 - 更新 - - - 刪除此應用程式及所有相關資料(包括保存資料)。 - 刪除應用程式可能需要一段時間, -取決於它的大小和您的硬體。 - 確定要刪除此補丁嗎? - 確定要刪除此追加內容嗎? - 確定要刪除此授權嗎? - - - 重設上次遊玩時間 - - 更新履歷 - {} 版本 - - 有權 - 無權 - 等級 {} - 名稱 - 獲得獎盃 - 家長監控 - 更新日 - 檔案大小 - 版本 - 標題ID - 類別 - 數位版遊戲 - 遊戲補丁 - 上次使用時間 - 使用時間 - 從來沒有 - - - {}秒 - {}分鐘{}秒 - {}小時{}分鐘{}秒 - {}天{}小時{}分鐘{}秒 - {}週{}天{}小時{}分鐘{}秒 - - - - - 發生錯誤。 -錯誤編號:{} - 取消 - 關閉 - 無法載入。 - 無法保存。 - 刪除 - 錯誤 - 檔案已毀損。 - 請啟用麥克風。 - - 請稍候…… - 搜尋 - 全選 - - 提交 - - - 星期日 - 星期一 - 星期二 - 星期三 - 星期四 - 星期五 - 星期六 - - - 1月 - 2月 - 3月 - 4月 - 5月 - 6月 - 7月 - 8月 - 9月 - 10月 - 11月 - 12月 - - - 1月 - 2月 - 3月 - 4月 - 5月 - 6月 - 7月 - 8月 - 9月 - 10月 - 11月 - 12月 - - - - 1日 - 2日 - 3日 - 4日 - 5日 - 6日 - 7日 - 8日 - 9日 - 10日 - 11日 - 12日 - 13日 - 14日 - 15日 - 16日 - 17日 - 18日 - 19日 - 20日 - 21日 - 22日 - 23日 - 24日 - 25日 - 26日 - 27日 - 28日 - 29日 - 30日 - 31日 - - - - 1日 - 2日 - 3日 - 4日 - 5日 - 6日 - 7日 - 8日 - 9日 - 10日 - 11日 - 12日 - 13日 - 14日 - 15日 - 16日 - 17日 - 18日 - 19日 - 20日 - 21日 - 22日 - 23日 - 24日 - 25日 - 26日 - 27日 - 28日 - 29日 - 30日 - 31日 - - 隱藏獎盃 - 1小時前 - 1分鐘前 - Bronze(銅) - Gold(金) - Platinum(白金) - Silver(銀) - {}小時前 - {}分鐘前 - - - - - 未知 - 無法啟動 - 可以啟動 - 有圖像 - 可進入選單 - 可進入遊戲- - 可進入遊戲+ - 可遊玩 - - - - - 無法獲取當前相容性資料庫,請檢查防火墻/網際網路接入後再重試。 - 無法下載更新於{}的應用程式相容性資料庫,請稍後重試。 - 無法讀取已下載更新於{}的應用程式相容性資料庫。 - 相容性資料庫已成功從{}更新至{}。 - -已列出{}個新的應用程式,使總數達到{}個! - 相容性資料庫已成功從{}更新至{}。 - -已列出{}個應用程式! - 已成功下載並載入{}更新的相容性資料庫。 - -已列出{}個應用程式! - - - - 正在編譯著色器 - 已編譯{}個管線 - 已編譯{}個著色器 - - - - - 即將刪除您選擇的應用程式及所有相關資料(包括保存資料)。 - 找不到內容。 - - - 刪除目前選擇的保存資料。 - 找不到保存資料。 - - 主題 - 可用容量 - 全解除 - - - - 已接入{}個控制器 - 編號 - 體感支援 - 重新繫結控制 - LED顔色 - 使用自訂顔色 - 勾選此項可為控制器的LED使用自訂顔色。 - - - - 未接入相容的控制器。 -請接入與SDL3相容的控制器。 - 停用體感 - 使用內置設備傳感器 - 重設控制器繫結 - - - - - 按鍵對應 - 左操作桿上 - 左操作桿下 - 左操作桿右 - 左操作桿左 - 右操作桿上 - 右操作桿下 - 右操作桿右 - 右操作桿左 - 方向按鈕上 - 方向按鈕下 - 方向按鈕右 - 方向按鈕左 - 方形按鈕 - 交叉按鈕 - 圓形按鈕 - 三角按鈕 - START按鈕 - SELECT按鈕 - PS按鈕 - L1按鈕 - R1按鈕 - 僅在PS TV模式下有效。 - L2按鈕 - R2按鈕 - L3按鈕 - R3按鈕 - 圖形使用者介面 - 全螢幕 - 切換觸控 - 在背觸和前觸之間切換。 - 切換圖形使用者介面可見性 - 在應用程式執行時,在螢幕頂部顯示和隱藏圖形使用者介面之間切換。 - 雜項 - 切換紋理替換 - 截圖 - 捏合變更按鈕 - 交替捏合向內按鈕 - 交替捏合向外/伸縮按鈕 - 此按鈕已繫結至其他鍵或已被保留。 - 重設控制繫結 - - - - - 正在準備啟動應用程式…… - - - - 確定要取消刪除嗎? - 已刪除。 - 確定要刪除此保存資料嗎? - - -
詳細內容
- 更新日 -
- - 確定要取消載入嗎? - 找不到保存資料。 - 已載入。 - 載入中…… - 確定要載入此保存資料嗎? - - - 確定要取消保存嗎? - 無法保存。 -記憶卡的可用容量不足。若要保存應用程式的進度,需準備{}以上的可用容量。 - -若要準備可用容量,請按下PS按鈕暫停此應用程式,並刪除其他應用程式或內容。 - 記憶卡的可用容量不足。 -若要繼續執行應用程式,請準備{}以上的可用容量。 - -請按下PS按鈕暫停此應用程式,並刪除其他應用程式或內容。 - 新的保存資料 - 已保存。 - 確定要保存資料嗎? - 保存中…… - 保存中…… -請勿關閉電源或結束應用程式。 - 確定要覆寫此保存資料嗎? - -
-
- - - 結束下列應用程式。 - 刪除的資料: - - - - 篩選 - 應用程式排列依 - 全部 - 依地區 - 美國 - 歐洲 - 日本 - 亞洲 - 依類型 - 商業 - 自製 - 依相容性狀況 - 版本 - 類別 - 相容性 - 上次使用時間 - 更新 - - - - 已追加應用程式至主頁畫面。 - 全部刪除 - 刪除最新資訊。 - 無法安裝。 - 已安裝。 - 安裝中…… - 找不到最新資訊。 - 已獲得獎盃! - - - - 返回 - 已完成初期設定。 -請盡情享受Vita3K。 - 請選擇語言。 - 請選擇存放路徑。 - Vita3K需要完整的檔案存取權限。 -點擊"授予存取權限"以繼續。 - 授予存取權限 - 當前模擬器路徑 - 更改模擬器路徑 - 重置模擬器路徑 - 安裝韌體。 - 強烈建議安裝全部韌體檔案。 - 已安裝: - 下載字型套件 - 安裝韌體檔案 - 請選擇介面設定。 - 資訊欄可見 - 勾選此項在應用程式選擇頁面顯示資訊欄。 -資訊欄是時鐘、電量以及資訊中心。 - Live Area應用程式螢幕 - 勾選此項在按下應用程式時預設開啟Live Area頁面。 -如果停用,請右鍵點選應用程式將其開啟。 - 網格模式 - 勾選此項將應用程式列表設定為像PS Vita一樣的網格模式。 - 應用程式圖標大小 - 選擇您喜歡的圖標大小。 - 已完成。 - 繼續 - - - - - 韌體安裝 - 安裝中,請稍候…… - 韌體已成功安裝。 - 韌體版本: - 不存在韌體字型套件,請下載並安裝。 - 一些應用程式需要韌體字型套件,也需要亞洲區域字型支援。(強烈建議安裝) - 刪除韌體安裝檔案? - - - 請選擇授權類型 - 選擇work.bin/rif - 輸入zRIF - 輸入zRIF金鑰 - 請在此處輸入您的zRIF - Ctrl+C複製,Ctrl+V貼上。 - 刪除pkg檔案? - 刪除work.bin/rif檔案? - 無法安裝該包。 -請檢查pkg以及work.bin/rif檔案或zRIF金鑰。 - - - 請選擇安裝類型 - 選擇檔案 - 選擇目錄 - 已找到{}個相容內容的封存檔。 - 已成功安裝{}個封存檔中的內容: - 更新應用程式至: - 無法安裝{}個封存檔中的內容: - 找不到{}個封存檔中的相容內容: - 刪除封存檔? - - - 已成功安裝授權。 - 無法安裝授權。 -請檢查work.bin/rif檔案或zRIF金鑰。 - - - 重新安裝此內容? - 已安裝過此內容。 - 您想要重新安裝並覆寫現有的資料嗎? - - - - - 啟動 - 繼續 - - 在控制設定中使用鍵盤組態集 - 未檢測到韌體,強烈建議安裝 - 未檢測到韌體字型,建議在“Live Area”中安裝字型套件 - Live Area說明 - 瀏覽應用程式清單 - 方向鍵、左操作桿、滾輪上/下或使用滾動條 - 啟動應用程式 - 按下啟動或按交叉按鈕 - 應用程式執行時顯示/隱藏Live Area - 按PS按鈕 - 解除Live Area - 按下Esc或按圓形按鈕 - 說明書 - 瀏覽頁面 - 方向鍵、左操作桿、滾輪上/下或使用滾動條、按下</> - 隱藏/顯示按鈕 - 按下左鍵或按三角按鈕 - 解除說明書 - 按下Esc或按PS按鈕 - - - - - 無法讀取“{}”, -請檢查vita3k.log查閲控制臺輸出的詳細資訊。 -1、您是否安裝了韌體? -2、重新傾印您已擁有的PS Vita應用程式/遊戲,並在Vita3K中安裝。 -3、如果您想要安裝/執行Vitamin是不支援的。 - - - - 手把虛擬按鈕 - 在遊戲中顯示手把虛擬按鈕 - 隱藏手把虛擬按鈕 - 修改手把虛擬按鈕 - 虛擬按鈕縮放 - 虛擬按鈕不透明度 - 重設手把佈局 - 顯示前/後觸屏切換按鈕。 - 僅當啟用PSTV模式時,才會顯示L2/R2按鈕。 - - - - 平均 - 最低 - 最高 - - - - - 原音 - - 尋找PSVita自訂主題 - - 名稱 - 提供者 - 更新日 - 檔案大小 - 版本 - 內容ID - - 刪除此主題。 - - - 圖像 - 添加圖像 - - - 刪除背景 - 添加背景 - - - - - 年/月/日 - 日/月/年 - 月/日/年 - - - 12小時制 - 24小時制 - - - - 系統語言 - - - - 丹麥文 - 德文 - 英文(英國) - 英文(美國) - 西班牙文 - 法文 - 義大利文 - 荷蘭文 - 挪威文 - 波蘭文 - 葡萄牙文(巴西) - 葡萄牙文(葡萄牙) - 俄文 - 芬蘭文 - 瑞典文 - 土耳其文 - - - - - - - - - 模組模式 - 自動 - 選擇自動模式會使用預設的模組列表。 - 自動&手動 - 選擇此模式會載入預設模組以及從列表中所選定的模組。 - 手動 - 選擇手動模式會從列表中載入所選定的模組。 - 模組列表 - 選擇您所需的模組。 - 搜尋模組 - 清除列表 - 沒有模組存在。 -請下載並安裝最新的PS Vita韌體。 - 更新列表 - - - 啟用最佳化 - 勾選此項啟用額外的CPU JIT最佳化。 - - - 重設 - 後端渲染器 - 選擇您喜歡的後端渲染器。 - GPU(重啟後應用) - 選擇Vita3K應使用的GPU。 - 添加自訂驅動 - 下載自訂驅動 - 移除自訂驅動 - 標準 - - 渲染精度 - 垂直同步 - 停用垂直同步可以解決某些遊戲的速度問題。 -建議保留啟用它以避免畫面撕裂。 - 停用表面同步 - 提升速度,勾選此項停用處理器與顯示卡之間的表面同步。 -某些遊戲需要表面同步。 -如果停用(特別是在放大時),則可大幅提升效能。 - 非同步管線編譯 - 允許在多個並行執行緒上同時編譯管線。 -這可以減少管線編譯卡頓,但代價是會出現短暫的圖形故障。 - 最鄰近 - 雙線性 - 雙三次 - 螢幕過濾 - 設定要應用的後處理過濾器。 - 內部解析度放大 - 為Vita3K啟用放大效果。 -實驗性:不保證遊戲能在超過1倍時正確渲染。 - 各向異性過濾 - 各向異性過濾是一種提高表面圖像質量的技術, -它們相對於觀察者是傾斜的, -它沒有缺點,但會影響效能。 - 紋理替換 - 導出紋理 - 導入紋理 - 紋理導出格式 - FPS修改 - 遊戲修改允許一些以30FPS執行的遊戲在模擬器上以60FPS的速度執行。 -請注意這只是一個修改,僅適用於某些遊戲。 -在其他遊戲中,它可能沒有效果或使其出現兩倍速。 - 禁用 - 雙重緩衝 - 外部主機 - 頁表 - 原生緩衝 - 內存映射方式 - 內存映射提高了性能,減少內存使用量,並修復了許多圖形問題。 -但是它在某些GPU上可能不穩定。 - 啟用睿頻模式 - 提供了一種方法提來强制GPU以允許的最高時鐘頻率運行(溫控仍然適用)。 - 著色器 - 使用著色器快取 - 勾選此項啟用著色器快取以在遊戲啟動時對其進行預編譯。 -取消勾選則停用該功能。 - 使用Spir-V著色器(已棄用) - 將生成的Spir-V著色器直接傳遞給驅動程式。 -請注意,一些有益的擴充功能將被停用, -並非所有顯示卡都與此相容。 - 清除著色器快取以及記錄 - - - - 確認按鈕分配 -請選擇您的“確認”按鈕。 - 這是在應用程式對話方塊中用作“確認”的按鈕。 -某些應用程式不使用此功能並獲取預設確認按鈕。 - - × - PS TV模式 - 勾選此項啟用PS TV模擬模式。 - Show模式 - 勾選此項啟用Show模式。 - Demo模式 - 勾選此項啟用Demo模式。 - - - 以全螢幕啟動應用程式 - 追蹤 - 資訊 - 警告 - 錯誤 - 嚴重 - 關閉 - 記錄級別 - 選擇您喜歡的記錄級別。 - 封存記錄 - 勾選此項啟用封存記錄。 - 啟用Discord Rich Presence以在Discord上展示您正在執行的應用程式。 - 紋理快取 - 取消勾選此項停用紋理快取。 - 顯示編譯著色器 - 取消勾選此項停用編譯著色器對話方塊的顯示。 - 顯示觸控板游標 - 取消勾選此項停用螢幕上觸控板游標的顯示。 - 記錄相容性警告 - 勾選此項啟用GitHub議題的相容性警告的記錄。 - 檢查更新 - 啟動時自動檢查更新。 - 文件加载延迟 - 文件加載延遲(毫秒)。 -某些遊戲需要設置此項,以防止文件加載速度過快,與實際硬體不符(例如:寂靜嶺)。 -預設值為0毫秒。 - 效能圖層 - 在螢幕上以圖層的形式顯示效能資訊。 - 最少 - - 中等 - 最多 - 細節 - 選擇您喜歡的效能圖層細節。 - 左上 - 頂部中間 - 右上 - 左下 - 底部中間 - 右下 - 位置 - 選擇您喜歡的效能圖層位置。 - 勾選此項在區分大小寫的檔案系統上啟用不區分大小寫的路徑查詢。 -重啟時復位 - 允許模擬器嘗試在非Windows平臺上不區分大小寫的檔案搜尋。 - 模擬系統存儲資料夾 - 當前模擬器路徑: - 更改模擬器路徑 - 更改Vita3K模擬器資料夾路徑。 -您需要手動將舊資料夾移動到新位置。 - 重置模擬器路徑 - 重置Vita3K模擬器為預設路徑。 -您需要手動將舊資料夾移動到預設位置。 - 自訂組態設定 - 清除自訂組態 - 截圖圖像類型 - - 截圖格式 - - - 圖形使用者介面可見 - 勾選此項在啟動應用程式後顯示圖形使用者介面。 - 資訊欄可見 - 勾選此項在應用程式選擇頁面顯示資訊欄。 - 圖形使用者介面語言 - 選擇您的使用者語言。 - 顯示資訊訊息 - 取消勾選此項只在記錄中顯示資訊訊息。 - 顯示系統應用程式 - 取消勾選此項停用從主頁畫面中顯示的系統應用程式。 -它們將僅顯示在主選單欄中。 - Live Area應用程式螢幕 - 勾選此項在按下應用程式時預設開啟Live Area頁面。 -如果停用,請右鍵點選應用程式將其開啟。 - 拉伸顯示區域 - 勾選此項擴大顯示區域以適應螢幕尺寸。 - 全屏高畫質精準渲染 - 對於螢幕比例為16:9的高畫質顯示器,勾選此項可在全屏模式下獲得畫素級精準渲染, -但代價是螢幕頂部和底部會有輕微裁剪。 - 網格模式 - 勾選此項將應用程式列表設定為網格模式。 - 應用程式圖標大小 - 選擇您喜歡的圖標大小。 - 字型支援 - 亞洲區域 - 勾選此項啟用對中文以及韓文的字型支援。 -啟用此功能將佔用更多記憶體,並且需要您重啟模擬器才能生效。 - 一些應用程式需要韌體字型套件 -以及在圖形使用者介面中的亞洲區域字型支援。 -通常也強烈建議將其用於圖形使用者介面。 - 主題&背景 - 當前主題內容ID: - 重置原音主題 - 使用主題背景 - 清除使用者背景 - 當前主頁畫面的背景: - 重置主頁畫面的背景 - 背景透明度 - 選擇您喜歡的透明背景效果。 -滑到最小不透明以及最大為透明。 - 背景延遲 - 選擇更改背景前的延遲秒數。 - 開始畫面延遲 - 選擇返回開始畫面前的延遲秒數。 - - - 已登入PSN - 如果勾選此項,遊戲將認為使用者已接入至PSN(但為離線)。 - IP位址 - 選擇在adhoc中使用的IP位址。 - 子網路遮罩 - 啟用HTTP - 勾選此項啟用遊戲在網際網路上使用HTTP協議。 - HTTP超時嘗試 - 當伺服器沒有響應時要進行多次嘗試。 -如果您有非常不穩定或是非常慢的網路可能會很有用。 - HTTP超時休眠 - 當伺服器沒有響應時嘗試進入休眠時間。 -如果您有非常不穩定或是非常慢的網路可能會很有用。 - HTTP讀取終止嘗試 - 當沒有更多資料可讀取時,嘗試多次可以提升效能, -但如果您的網路很糟糕,則會導致遊戲不穩定。 - HTTP讀取終止休眠 - 嘗試在沒有更多資料可讀取時進入休眠時間,這可以提升效能, -但如果您的網路很糟糕,則會導致遊戲不穩定。 - - - 導入記錄 - 記錄模組導入符號。 - 導出記錄 - 記錄模組導出符號。 - 著色器記錄 - 記錄在每次繪制呼叫中使用的著色器。 - Uniform記錄 - 記錄著色器uniform的名稱和值。 - 保存顔色表面 - 保存顔色表面到檔案。 - ELF提取 - 將載入的程式碼提取為ELF。 - 驗證層(要求重啟) - 啟用Vulkan驗證層。 - 取消監視程式碼 - 監視程式碼 - 取消監視記憶體 - 監視記憶體 - 取消監視導入呼叫 - 監視導入呼叫 - - 保存並重啟 - 保存並應用 - 保存並關閉 - 按下保存保留您的更改。 - - - - 瀏覽器 - 網路瀏覽器 - 獎盃 - 獎盃收藏 - 設定 - 內容管理 - - - - 刪除該使用者已保存的全部獎杯資訊。 - 刪除獎盃 - 刪除該使用者已保存的獎杯資訊。 - 未解鎖 -
詳細內容
- 獲得日 - 名稱 - 找不到獎盃。 -遊玩支援獎盃機能的應用程式即可獲得獎盃。 - 未獲得 - 原始 - 排列 - 獎盃總數 - Grade(階級) - 達成率 - 更新日 - 高級 - 展示隱藏獎盃 -
- - - 選擇使用者 - 建立使用者 - 已建立以下使用者。 - 編輯使用者 - 開啟使用者資料夾 - 此名稱已被使用。 - 刪除使用者 - 請選擇要刪除的使用者。 - 將刪除以下使用者。 - 若刪除使用者,該使用者的保存資料、獎盃也會一併被刪除。 - 刪除使用者。 -確定要繼續嗎? - 已刪除使用者。 - 選擇個人造型 - 重置個人造型 - 確定 - 自動登入使用者 - - - - 發現新版的Vita3K。 - 返回 - 確定要取消更新嗎? -選擇取消後,下次會從中斷處開始下載。 - 下載中… -下載完成後,Vita3K會自動重新啟動,並安裝新的更新。 - 無法更新。 - 剩下 {}分鐘 - 繼續 - 剩下 {}秒 - 確定要更新Vita3K嗎? - 已安裝新版的Vita3K。 - 已安裝最新版本的Vita3K。 - {} 版本的追加機能 - 作者 - 註解 - 更新 - {} 版本 - - - - Vita3K PlayStation Vita模擬器 - Vita3K是使用C++編寫的開源PlayStation Vita模擬器,適用於Windows、Linux、macOS以及Android。 - 目前模擬器仍處於開發階段,因此非常感謝任何回饋和測試。 - 要開始使用,請安裝全部PS Vita韌體檔案。 - 下載預裝韌體 - 下載韌體 - 下載韌體字型套件 - 有關如何設定Vita3K的全方位指南可在 - 快速入門 - 頁面中找到。 - 查閱商業遊戲和自製程式相容性清單以檢視目前可執行的內容。 - 商業遊戲相容性清單 - 自製程式相容性清單 - 歡迎參與貢獻! - 可以在#help頻道中取得額外支援 - Vita3K不縱容盜版,您必須傾印您自己擁有的遊戲。 - 下次顯示 - -
diff --git a/lang/user/id.xml b/lang/user/id.xml deleted file mode 100644 index 724f57525..000000000 --- a/lang/user/id.xml +++ /dev/null @@ -1,855 +0,0 @@ - - - - - - Buka Pref Path - Buka Textur Path - Install Firmware - Install .pkg - Install .zip, .vpk - Install lisensi - Keluar - - - Apl Terakhir Yg Digunakan - - - Thread - Semafor - Mutexe - Mutexe Ringan - Variabel Kondisi - Variabel Kondisi Ringan - Event Bendera - Alokasi Memory - Membongkar - - - Perguna Menager - - - Keyboard Kontrol - - - Selamat Datang - - - - - Vita3K: Emulator PS Vita/PS TV. Emulator PS Vita/PS TV fungsional pertama di dunia. - Vita3K adalah emulator PlayStation Vita/PlayStation TV sumber terbuka eksperimental yang ditulis dalam C++ untuk sistem operasi Windows, Linux, macOS dan Android. - Special credit: vita3k icon dirancang oleh: - Jika Kamu tertarik untuk berkontribusi, lihat kami di GitHub: - Jika kamu ingin mendukung kami, Anda dapat memberikan donasi atau subscribe kami: - Vita3K Staff - Developer - Kontributor - Pendukung - - - - - Cek state Aplikasi - Salin Ringkasan Vita3K - Buka laporan state - Buat state laporan - Perbarui database - - - Nama dan Judul ID - Ringkasan Apl - - Buat Pintasan - - Bikin - Hapus - - - Buka Folder - lisensi - Cache Shader - Catatan Shader - - - Aplikasi ini dan semua data terkait, termasuk data yang disimpan, akan dihapus. - Menghapus aplikasi mungkin memakan waktu cukup lama -tergantung pada ukuran dan perangkat keras Kamu. - Apakah Kamu ingin menghapus data yg dimasukkan ini? - Apakah kamu ingin menghapus lisensi ini? - - History Perbarui - Versi {} - - Memenuhi Syarat - Gak Memenuhi Syarat - Level {} - Nama - Perolehan Trofi - Pengawasan Ortu - Telah Diperbarui - Ukuran - Versi - Judul ID - Waktu Terakhir Perguna - Waktu Perguna - Belum Pernah - - - {}d - {}m:{}s - {}j:{}m:{}d - {}h:{}j:{}m:{}d - {}m:{}h:{}j:{}m:{}d - - - - - Terjadi kesalahan. -Kode kesalahan: {} - Batal - Tutup - Tidak dapat load file. - Tidak dapat menyimpan file. - Hapus - Kekeliruan - File ini udah corrupt. - Nonaktifkan microphone. - Gak - Mohon Tunggu... - Pencarian - Pilih Semua - - Kirim - Iya - - Minggu - Senin - Selasa - Rabu - Kamis - Jumat - Sabtu - - - January - February - Maret - April - Mei - Juni - Juli - Agustus - September - Oktober - November - Desember - - - january - february - maret - april - mei - juni - juli - agustus - september - oktober - november - desember - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Trofi Tersembunyi - 1 Jam Yg Lalu - 1 Menit Yg Lalu - Perunggu - Emas - Perak - {} Jam Yg Lalu - {} Menit Yg Lalu - - - - - Gak Diketahui - Gak bisa - Bootable - Intro - Menu - Ingame - - Ingame + - Playable - - - - - Gagal mendapatkan database kompatibilitas saat ini, periksa akses firewall/internet, coba lagi nanti. - Gagal mengunduh database kompatibilitas Aplikasi yang di-update di: {}, coba lagi nanti. - Gagal memuat database kompatibilitas Aplikasi yang diunduh diupdate di: {}. - Database kompatibilitas sukses di-update dari {} ke {}. - -{} aplikasi baru terdaftar, sehingga totalnya menjadi {}! - Database kompatibilitas sukses di-update dari {} ke {} - -{} aplikasi terdaftar! - Database kompatibilitas yang di-update di {} telah sukses diunduh dan dimuat. - -{} aplikasi dilistkan! - - - - Mengkompilasi Shader - {} pipe textur dikompilasi - {} shader dikompilasi - - - - - Aplikasi yang dipilih dan semua data terkait, termasuk data yang disimpan, akan dihapus. - Gak Ada Item Konten. - - - Item data tersimpan yang dipilih akan dihapus. - Disini gak ada save data. - - Tema - Penyimpanan Yg Kosong - Selesaikan Semua - - - - Kontroler Tersambung - Bil - Warna LED - Gunakan Warna Custom - Centang kotak ini untuk menggunakan warna custom untuk LED kontroller. - Merah - Hijau - Biru - Gak ada kontroler kompatibel yang tersambung. -Sambungkan Kontroleryang kompatibel dengan SDL3. - Gunakan builtin device motion sensor - Reset Ulang Pengikatan Kontroller - - - - - Mapped tombol - Analog Kiri Atas - Analog Kiri Bawah - Analog Kiri Kanan - Analog Kiri Kiri - Analog Kanan Atas - Analog Kanan Bawah - Analog Kanan Kanan - Analog Kanan Kiri - D-pad Atas - D-pad Bawah - D-pad Kanan - D-pad Kiri - Tombol Bulat - Tombol X - Tombol Kotak - Tombol segitiga - Tombol start - Tombol Select - Tombol PS - Tombol L1 - Tombol R1 - Only in PS TV mode. - Tombol L2 - Tombol R2 - Tombol L3 - Tombol R3 - Layar Penuh - Alihkan Sentuh - Beralih antara sentuh belakang dan sentuh layar. - Alihkan Visibilitas GUI - Beralih antara menampilkan dan menyembunyikan GUI di bagian atas layar saat aplikasi berjalan. - Kekeliruan - Kuncinya digunakan untuk binding lain atau dicadangkan. - - - - - Persiapan untuk memulai aplikasi... - - - - Kamu yakin untuk batalkan penghapusan? - Penghapusan Berhasil. - Kamu yakin untuk hapus save data ini? - - -
Detail
- Update telah selesai -
- - Kamu yakin untuk batalkan loading? - Disini gak ada save data. - Loading Berhasil. - Loading... - Kamu yakin untuk load di save data ini? - - - Kamu yakin untuk batalkan saving? - Tidak dapat menyimpan file. -Tidak ada cukup ruang kosong di kartu memori. - Tidak ada cukup ruang kosong di kartu memori. - Save Data Baru - Simpan Telah Berhasil. - Kamu yakin save di data ini? - Ngesave… - Ngesave… -Jangan matikan sistem atau tutup aplikasi ini. - Kamu yakin untuk timpa di save data ini? - -
-
- - - Aplikasi berikut akan ditutup. - Data telah dihapus: - - - - Penyesuaian - Urutan Aplikasi dari - Semua - Dari Region - Europa - Jepang - Dari Type - Komersial - Berdasarkan State Kompatibilitas - Waktu Terakhir - Barui - - - - Aplikasi telah ditambahkan ke layar beranda. - Hapus Semua - Notifikasi akan dihapus. - Tidak dapat menginstal. - Instalasi selesai. - Menginstalasi... - Tidak ada notifikasi. - Kamu telah mendapatkan trofi! - - - - - Menginstalasi dalam progress, mohon tunggu... - Firmware telah selesai instalasi. - Versi firmware: - Tidak ada paket font firmware. -Silakan unduh dan instal. - Paket font firmware diperlukan untuk beberapa -aplikasi dan juga untuk dukungan font regional -Asia. (Umumnya Direkomendasikan) - Kamu ingin menghapus file firmware yang telah diinstal? - - - Pilih tipe lisensi - Pilih work.bin/rif - Masukkan zRIF - Masukkan zRIF key - Kamu dimohon masukkan zRIF disini - Ctrl + C untuk menyalin, Ctrl + V untuk menempel. - Yakin untuk hapus file pkg? - Yakin untuk hapus file work.bin/rif? - - - Pilih tipe install - Pilih File - Pilih Direktori - {} arsip ditemukan dengan konten yang kompatibel. - {} arsip konten berhasil diinstal: - Update Aplikasi ke: - Gagal install konten arsip - Tidak ditemukan konten yang kompatibel di - {} arsip: - Hapus arsip? - - - Telah selesai instalasi lisensi. - - - Instal ulang konten ini? - Konten ini sudah instalasi - Apakah kamu ingin reinstal dan menimpa data yang ada? - - - - - Mulai - Lanjutkan - - Menggunakan set konfigurasi untuk keyboard dalam pengaturan kontrol - Firmware tidak terdeteksi. Instalasi sangat dianjurkan - Font firmware tidak terdeteksi. Instalasi dianjurkan untuk teks font di Live Area - Live Area Bantuan - Browser dalam daftar aplikasi - D-pad, Analog Kiri, Geser ke Atas/Bawah atau menggunakan Slider - Mulai Aplikasi - Geser ke Atas/Bawah atau menggunakan Penggeser - Tampilkan/Sembunyikan Live Area selama menjalankan aplikasi - Tekan pada PS - Live Area Keluar - Klik Esc atau Tekan Lingkaran - Bantuan Manual - Browser halaman - D-pad, Analog Kiri di Kiri/Kanan atau Geser ke Atas/Bawah atau Klik - Sembunyikan/Tampilkan tombol - Klik kanan - Perbesar jika tersedia - Double Klik Kiri - Gulir di perbesar - Geser Atas/Bawah - Keluar dari Bantuan Manual - Klik Esc atau Tekan di PS - - - - - Gagal untuk memuat "{}". -Periksa vita3k.log untuk melihat keluaran konsol untuk detailnya. -1. Apakah Kamu sudah menginstal firmware? -2. Buat PS Vita aplikasi/game Kamu sendiri dan instal di Vita3K. -3. Jika Kamu ingin menginstal atau mem-boot Vitamin, tidak didukung. - - - - Tampilkan Overlay gamepad digame - Sembunyikan Overlay Gamepad - Modif Overlay Gamepad - Skala Overlay - Tingkatan Kegelapan - Reset Ulang Gamepad - Tampilkan tombol sakelar layar sentuh depan/belakang - Trigger L2/R2 hanya akan ditampilkan jika mode PSTV diaktifkan. - - - - - Bawaan - - Temukan Tema Kustom PSVita - - Nama - Pemberi - Update - ukuran - Versi - Konten ID - - Tema ini akan dihapus. - - - Gambar - Tambahkan GB - - - Hapus Background - Tambahkan Background - - - - - TTTT/BB/HH - HH/BB/TTTT - BB/HH/TTTT - - - Atur 12-Jam - Atur 24-Jam - - - - Bahasa System - - - - Denmark - Jerman - Inggris (United Kingdom) - Inggris (United States) - Spanyol - Perancis - Italia - Belanda - Norwegia - Polandia - Portugis (Brazil) - Portugis (Portugal) - Rusia - Finlandia - Swedia - Turki - - - - - - - - - Mode Modul - Daftar Modul - Pilih modul yang Kamu inginkan. - Pencarian Modul - Bersihkan Daftar - Tidak ada modul. -Silakan download dan install firmware PS Vita terakhir. - Perbarui Daftar - - - Aktifkan Pengoptimalan - Centang kotak untuk mengaktifkan optimasi CPU JIT tambahan. - - - Bagian Belakang Perender - Pilih perender backend pilihan kamu. - GPU (Harus ulang lagi aplikasinya) - Pilih GPU Vita3K yg harus dijalankan. - Pasang custom driver - Hapus custom driver - Tinggi - Perender Akurasi - Menonaktifkan V-Sync dapat memperbaiki masalah kecepatan di beberapa game. -Disarankan untuk tetap mengaktifkannya untuk menghindari robekan visual. - Nonaktifkan Edit Sinkronisasi - Speed hack, centang kotak untuk menonaktifkan sinkronisasi permukaan antara CPU dan GPU. -Edit Sinkronisasi diperlukan oleh beberapa game. -Memberikan peningkatan kinerja yang besar jika dinonaktifkan (khususnya saat peningkatan skala aktif). - Terdekat - Penyesuaian Layar - Setel filter pasca-pemrosesan untuk diterapkan. - Pilih Resolusi Internal - Gunakan Pelukisan untuk Vita3K. -Eksperimental: Game tidak dijamin akan dirender dengan baik lebih dari 1x. - Penyaringan Anisotropik - Penyaringan anisotropik adalah teknik untuk meningkatkan kualitas gambar permukaan -yang miring relatif terhadap pemirsa. -Ini tidak memiliki kelemahan tetapi dapat mempengaruhi pada Performa. - Penggantian Tekstur - Export Textur - Import Textur - Format Pengekspor Tekstur - Paksa FPS - Memaksa game yang memungkinkan beberapa game yang berjalan pada 30 FPS dijalankan pada 60 FPS di emulator. -Perhatikan bahwa ini adalah peretasan dan hanya akan berfungsi pada beberapa game. -Di game lain, ini mungkin tidak berpengaruh atau membuatnya berjalan dua kali lebih cepat. - Dinonaktifkan - Ganda buffer - Tabel halaman - Pemetaan memory - Pemetaan memory itu bisa naikkan performanya. -kurangi pemakaian memory dan kebanyakan fix di bug grafis nya -selain itu banyak perangkat gak sama stabilnya di bahkan sesama cpu. - Shader - Pake Shader Cache - Centang kotak untuk mengaktifkan cache shader untuk melakukan pra-kompilasi saat startup game. -Hapus centang untuk menonaktifkan fitur ini. - Gunakan Shader Spir-V (tidak digunakan lagi) - Berikan shader Spir-V yang dihasilkan langsung ke pengemudi. -Perhatikan bahwa beberapa ekstensi bermanfaat akan dinonaktifkan, -dan tidak semua GPU kompatibel dengan ini. - Bersihkan Shader Cache dan Log - - - - Masukkan penetapan tombol -Pilih tombol 'Masuk' kamu. - Ini adalah tombol yang digunakan sebagai 'Konfirmasi' dalam dialog aplikasi. -Beberapa aplikasi tidak menggunakan ini dan mendapatkan tombol konfirmasi default. - O - X - Mode PS TV - Centang kotak untuk mengaktifkan mode Emulasi PS TV. - Mode Show - Centang kotak untuk mengaktifkan mode Show. - Mode Demo - Centang kotak untuk mengaktifkan mode Demo. - - - Boot aplikasi dalam layar penuh - Jejak - Peringatan - Kekeliruan - Kritis - Mati - Tingkatan Log - Pilih tingkat log pilihan Kamu. - Log Arsip - Centang kotak untuk mengaktifkan Log Arsip. - Mengaktifkan Discord Rich Presence untuk menampilkan aplikasi apa yang Kamu jalankan di Discord. - Textur Cache - Hapus centang pada kotak untuk menonaktifkan cache tekstur. - Tampilkan Kompilasi Shader - Hapus centang pada kotak untuk menonaktifkan tampilan dialog kompilasi shader. - Tampilkan Kursor touchpad - Hapus centang pada kotak untuk menonaktifkan tampilan kursor touchpad di layar. - Peringatan Kompatibilitas Log - Centang kotak untuk mengaktifkan peringatan kompatibilitas log masalah GitHub. - Periksa pembaruan - Secara otomatis memeriksa pembaruan saat startup. - Detail performa dilayar - Menampilkan informasi performa dilayar sebagai overlay. - Rendah - Pilih detail hamparan performa pilihan kamu. - Diatas Kiri - Diatas Tengah - Diatas Kanan - Dibawah Kiri - Dibawah Tengah - Dibawah kanan - Posisi di layar - Pilih posisi hamparan performa pilihan kamu. - Centang untuk mengaktifkan penemuan jalur peka huruf besar-kecil pada sistem file peka huruf besar-kecil. -RESET PADA RESTART - Mencari file apa pun kasusnya -pada platform non-Windows. - Folder Penyimpanan Sistem Yang Ditiru - Letak emulator saat ini: - Ubah Letak Emulator - Ubah letak folder emulator Vita3K. -Anda perlu memindahkan folder lama ke lokasi baru secara manual. - Reset Letak Emulator - Reset letak emulator Vita3K ke default. -Kamu perlu memindahkan folder lama ke lokasi baru secara manual. - Pengaturan Konfigurasi Kustom - Bersihkan Konfigurasi Kustom - - - GUI Terlihat - Centang kotak untuk menampilkan GUI setelah mem-boot aplikasi. - Info Bar Terlihat - Centang kotak untuk menampilkan bar info di dalam pemilih aplikasi. - bahasa GUI - Pilih bahasa User kamu. - Tampilkan Pesan Info - Hapus centang pada kotak untuk menampilkan pesan info di log saja. - Tampilkan System Aplikasi - Hapus centang pada kotak untuk menonaktifkan tampilan aplikasi sistem di layar beranda. -Mereka hanya akan ditampilkan di bar menu utama. - Live Area Layar Aplikasi - Centang kotak untuk membuka Live Area secara default saat mengklik aplikasi. -Jika dinonaktifkan, klik kanan pada aplikasi untuk membukanya. - Penuhkan Layar - Centang kotak untuk memperbesar area layar agar sesuai dengan ukuran layar. - Mode Jaringan - Centang kotak untuk mengatur daftar aplikasi ke mode grid. - Ukuran Ikon Aplikasi - Pilih ukuran ikon pilihan Kamu. - Dukungan font - Region Asia - Centang kotak ini untuk mengaktifkan dukungan font untuk bahasa China dan Korea. -Mengaktifkannya akan menggunakan lebih banyak memori dan mengharuskan kami memulai ulang emulator. - Paket font firmware diperlukan untuk beberapa aplikasi -dan juga untuk dukungan font regional Asia di GUI. -Umumnya juga direkomendasikan untuk GUI. - Tema & Latar Belakang - Id konten tema saat ini: - Reset Ke Bawaan Tema - Gunakan background tema - Bersihkan Backgrounds Pengguna - Saat ini background mulai: - Reset Background mulai - Background Alpha - Pilih transparansi background pilihan kamu. -Nilai minimumnya buram dan maksimumnya transparan. - Penundaan untuk background - Pilih penundaan (dalam detik) sebelum mengubah background. - Penundaan untuk layar mulai - Pilih penundaan (dalam detik) sebelum kembali ke layar awal. - - - Masuk ke PSN - Jika dicentang, game akan menganggap user terhubung ke jaringan PSN (tetapi offline). - Aktifkan HTTP - .Centang kotak ini untuk mengaktifkan game menggunakan protokol HTTP di internet - Upaya Batas Waktu HTTP - Berapa banyak upaya yang harus dilakukan ketika server tidak merespons. -Bisa bermanfaat jika kamu memiliki internet yang sangat tidak stabil atau SANGAT LAMBAT. - HTTP Batas Waktu Tidur - Coba waktu tidur ketika server tidak menjawab. -Bisa bermanfaat jika kamu memiliki internet yang sangat tidak stabil atau SANGAT LAMBAT. - Upaya Akhir Baca HTTP - Berapa banyak upaya yang harus dilakukan ketika tidak ada lagi data untuk dibaca, -lebih rendah dapat meningkatkan performa tetapi dapat membuat game tidak stabil jika internet kamu cukup buruk. - HTTP Baca Akhir Tidur - Cobalah waktu tidur ketika tidak ada lagi data untuk dibaca, -lebih rendah dapat meningkatkan performa tetapi dapat membuat game tidak stabil jika internet kamu cukup buruk. - - - Impor Pencatatan - Simbol impor modul log. - Export Pencatatan - Simbol export modul log. - Pencatatan Shader - Log shader digunakan pada setiap panggilan undian. - Pencatatan Seragam - Catat nama dan nilai seragam shader. - Simpan permukaan warna. - Pembuangan ELF - Buang kode yang dimuat sebagai ELF. - Memvalidasi Lipatan (Diperlukan buatulang) - Aktifkan memvalidasi lipatan Vulkan. - Hapus Kode - Tonton Kode - Hapus Memory - Tonton Memory - Hapus Impor Panggilan - Tonton Impor Panggilan - - Simpan & BuatUlang - Simpan & Terapkan - Simpan & Tutup - Klik Simpan untuk menyimpan perubahan Kamu. - - - - Hapus Trofi - Informasi trofi yang disimpan pada pengguna ini akan dihapus. - dibuka -
Detail
- Diperoleh - Nama - Tidak ada piala. -Kamu bisa mendapatkan trofi dengan menggunakan aplikasi yang mendukung fitur trofi. - Tidak Diperoleh - Original - Menyortir - Trofi - Nilai - Progress - Telah Diupdate -
- - - Pilih user kamu - Bikin User - User berikut telah dibuat. - Buka Folder User - Nama ini sudah digunakan. - Hapus User - Pilih user yang ingin Kamu hapus. - User berikut akan dihapus. - Jika kamu menghapus user, data yang disimpan user tersebut, trofi akan dihapus. - User akan dihapus. -Apakah anda yakin ingin melanjutkan? - User telah dihapus. - Ganti Avatar - Konfirmasi - Automatis User Login - - - - versi terbaru udah Vita3K tersedia. - Kembali - Downloading... -Setelah pengunduhan selesai, Vita3K akan restart secara otomatis dan kemudian menginstal Vita3K baru. - Tidak dapat menyelesaikan update. - Lanjut - Apakah kamu ingin memperbarui Vita3K? - Versi Vita3K yang lebih baru sudah diinstal. - Versi terbaru Vita3K sudah diinstal. - Baru Fitur di Versi ini {} - Penulis - Komentar - Perbarui - Versi {} - - - - Emulator Vita3K PlayStation Vita - Vita3K adalah emulator PlayStation Vita sumber terbuka yang ditulis dalam C++ untuk Windows, Linux, macOS dan Android. - Emulator masih dalam tahap awal sehingga umpan balik dan pengujian apa pun sangat dihargai. - Panduan lengkap tentang cara menyiapkan Vita3K dapat ditemukan di - Mulai Cepat - halaman. - Cek kompatibilitas game Komersial dan Homebrew untuk melihat apa yang sedang -berjalan. - Daftar Kompatibilitas Komersial - Daftar Kompatibilitas Homebrew - Kontribusi dipersilakan! - Dukungan tambahan dapat ditemukan di saluran #help channel dari - Vita3K tidak memaafkan pembajakan. Kamu harus dump game kamu sendiri. - Tunjukkan lain kali - -
diff --git a/lang/user/ms.xml b/lang/user/ms.xml deleted file mode 100644 index 6f9b04ac6..000000000 --- a/lang/user/ms.xml +++ /dev/null @@ -1,871 +0,0 @@ - - - - - - Buka Laluan Pref - Buka Textures Laluan - Install Firmware - Install .pkg - Install .zip, .vpk - Install Lesen - Keluar - - - Apl Terakhir Digunakan - - - Benang - Semaphore - Mutexes - Mutex ringan - Pembolehubah Keadaan - Pembolehubah Keadaan Ringan - Bendera Event - Peruntukan Memori - Pembongkaran - - - Pengurusan Pengguna - - - Kawalan Keyboard - - - SelamatDatang - - - - - Vita3K: Emulator TV PS Vita/PS. Emulator TV PS Vita/PS berfungsi pertama di dunia. - Vita3K ialah emulator PlayStation Vita/PlayStation TV sumber terbuka eksperimen yang ditulis dalam C++ untuk sistem pengendalian Windows, Linux, macOS dan Android. - Kredit istimewa: Ikon Vita3K direka oleh: - Jika awak berminat untuk menyumbang, lihat kami: - Lawati laman web kami untuk maklumat lanjut: - Jika awak ingin menyokong kami, awak boleh membuat sumbangan atau melanggan kami: - Vita3K Staff - Pemaju - Penyumbang - Penyokong - - - - - Semak keadaan Apl - Salin Ringkasan Vita3K - Buka laporan state - Buat laporan state - Pangkalan data maklumat - - - Nama dan ID Tajuk - Ringkasan Apl - - Buat Shortcut - - Cipta - Alih keluar - - - Buka Folder - Lesen - Cache Shaders - Log Shaders - - - Kemas kini - - - Apl ini dan semua data berkaitan, termasuk data yang disimpan, akan dipadamkan. - Memadamkan Apl mungkin mengambil sedikit masa, -bergantung pada saiznya dan perkakasan awak. - Adakah awak mahu memadamkan data tambahan ini? - Adakah awak mahu memadamkan lesen ini? - - Kemas kini Sejarah - Versi {} - - Layak - Tak Layak - Tahap {} - Nama - Pendapatan Trofi - Kawalan Ibu Bapa - Dikemas Kini - Ukuran - Versi - ID Tajuk - Kali Terakhir Digunakan - Masa Yang Digunakan - Tak Pernah - - - {}d - {}m:{}d - {}j:{}m:{}d - {}h:{}j:{}m:{}d - {}a:{}h:{}j:{}m:{}d - - - - - Ralat berlaku. -Kod ralat: {} - Batal - Tutup - Tidak dapat memuatkan fail. - Tidak dapat menyimpan fail. - Padam - Ralat - Fail itu rosak. - Dayakan mikrofon. - Tak - Sila tunggu... - Cari - Pilih semua - - Hantar - Ya - - Ahad - Isnin - Selasa - Rabu - Khamis - Jumaat - Sabtu - - - Januari - Februari - Mac - April - Mei - Jun - Julai - Ogos - September - Oktober - November - Disember - - - januari - februari - mac - april - mei - jun - julai - ogos - september - oktober - november - disember - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Piala Tersembuny - 1 Jam Yang Lalu - 1 Minit Yang Lalu - Gangsa - Emas - Perak - {} Jam Yang Lalu - {} Minit Yang Lalu - - - - - Tak Diketahui - Tiada kuat - Bootable - Intro - Menu - Ingame - - Ingame + - Playable - - - - - Gagal mendapatkan pangkalan data keserasian semasa, semak akses firewall/internet, cuba lagi kemudian. - Gagal memuat turun pangkalan data keserasian Aplikasi yang dikemas kini di: {}, cuba sebentar lagi. - Gagal memuatkan pangkalan data keserasian Aplikasi yang dimuat turun dikemas kini di: {}. - Pangkalan data keserasian telah berjaya dikemas kini daripada {} ke {}. - -{} apl baharu disenaraikan, menjadikan jumlah keseluruhan kepada {}! - Pangkalan data keserasian telah berjaya dikemas kini daripada {} ke {}. - -{} apl disenaraikan! - Pangkalan data keserasian yang dikemas kini pada {} telah berjaya dimuat turun dan dimuatkan. - -{} apl disenaraikan! - - - - Menyusun Shaders - {} textures paip disusun - {} shader disusun - - - - - Apl yang dipilih dan semua data berkaitan, termasuk data yang disimpan, akan dipadamkan. - Tiada item kandungan. - - - Item data disimpan yang dipilih akan dipadamkan. - Tiada data yang disimpan. - - Tema - Ruang kosong - Kosongkan semua - - - - {} Kontroler Bersambung - Bil - Kawalan Rebind - Warna LED - Gunakan Warna Tersuai - Tandai kotak ini untuk menggunakan warna tersuai untuk LED Kontroler. - Merah - Hijau - Biru - Tiada Kontroler serasi disambungkan. -Sila sambungkan Kontroler yang serasi dengan SDL3. - Gunakan builtin device motion sensor - Tetapkan Semula Pengikat Kontroler - - - - - Butang dipetakan - Kiri analog atas - Kiri analog bawah - Kiri analog kanan - Kiri analog kiri - Kanan analog atas - Kanan analog bawah - Kanan analog kanan - Kanan analog kiri - D-pad atas - D-pad bawah - D-pad kanan - D-pad kiri - Butang kotak - Butang X - Butang bulatan - Butang segitiga - Butang mula - Butang Select - Butang PS - Butang L1 - Butang R1 - Hanya dalam mode PS TV. - Butang L2 - Butang R2 - Butang L3 - Butang R3 - Skrin penuh - Togol Sentuh - Bertukar antara sentuhan belakang dan sentuhan skrin. - Togol Keterlihatan GUI - Togol antara menunjukkan dan menyembunyikan GUI di bahagian atas skrin semasa apl sedang berjalan. - Macam-macam - Togol Penggantian Tekstur - Ambil Tangkapan Skrin - Ralat - Key digunakan untuk pengikatan lain atau ia dikhaskan. - - - - - Bersedia untuk memulakan Apl... - - - - Adakah Awak mahu membatalkan pemadaman? - Pemadaman selesai. - Adakah awak mahu memadamkan data yang disimpan ini? - - -
Butiran
- Dikemas Kini -
- - Adakah awak mahu membatalkan loading? - Tiada data yang disimpan. - Loading selesai. - Muatkan... - Adakah awak mahu memuatkan data yang disimpan ini? - - - Adakah awak mahu membatalkan simpanan? - Tidak dapat menyimpan fail. -Tiada ruang kosong yang mencukupi pada kad memori. - Tiada ruang kosong yang mencukupi pada kad memori. - Data Tersimpan Baharu - Saving selesai. - Adakah awak ingin menyimpan data? - Menyimpan... - Menyimpan... -Jangan matikan sistem atau tutup apl. - Adakah awak mahu menulis ganti data yang disimpan ini? - -
-
- - - Apl berikut akan ditutup. - Data untuk Dipadamkan: - - - - Penapis - Isih Apl Mengikut - Semua - Mengikut Region - Jepun - Mengikut Type - Komersil - Mengikut Keadaan Keserasian - Komp - Kali terakhir - MuatSemula - - - - Apl telah ditambahkan pada skrin utama. - Memadam semua - Notifikasi akan dipadamkan. - Tak dapat menginstal. - Instalasi selesai - Menginstalasi... - Tiada Notifikasi. - Awak telah memperoleh trofi! - - - - - Instalasi Firmware - Instalasi sedang dijalankan, sila tunggu... - Firmware berjaya Instalasi. - Firmware versi: - Tiada firmware font package hadir, sila muat turun dan pasangkannya. - Firmware font package diperlukan untuk sesetengah apl -dan juga untuk sokongan fon serantau Asia. (Secara Umum Disyorkan) - Padamkan fail Instalasi firmware - - - Pilih jenis lesen - Pilih work.bin/rif - Masukkan zRIF - Masukkan zRIF key - Sila masukkan zRIF awak di sini - Ctr + C untuk menyalin, Ctrl + V untuk menampal. - Padamkan fail pkg? - Padamkan fail work.bin/rif? - Gagal memasang pakej. -Sila semak fail pkg dan work.bin/rif atau key zRIF. - - - Pilih Jenis install - Pilih File - Pilih Direktori - {} arkib ditemui dengan kandungan yang serasi. - {} kandungan arkib berjaya Instalasi: - Kemas kini Apl kepada: - Gagal memasang {} kandungan arkib: - Tiada kandungan yang serasi ditemui dalam {} arkib: - Padamkan arkib? - - - Berjaya instalasi lesen. - Gagal memasang lesen. -Sila semak fail work.bin/rif atau zRIF key. - - - Pasang semula kandungan ini? - Kandungan ini sudah instalasi. - Adakah awak mahu memasang semula dan menulis ganti data sedia ada? - - - - - Mulakan - teruskan - - Menggunakan set konfigurasi untuk keyboard dalam tetapan kawalan - Firmware tidak dikesan. Instalasi amat disyorkan - Firmware font tidak dikesan. Menginstalasi disyorkan untuk fon teks dalam Live Area - Bantu Live Area - Semak imbas dalam senarai apl - D-pad, Analog kiri, Wheel in Atas/Bawah atau menggunakan Slider - Mulakan Apl - Klik pada Mula atau Tekan pada X - Tunjukkan/Sembunyikan Live Area semasa apl dijalankan - Tekan di PS - Keluar Live Area - Klik pada Esc atau Tekan pada Bulatan - Manual Bantuan - Semak imbas halaman - D-pad, Analog kiri, Wheel in Atas/Bawah atau menggunakan Slider, Klik pada lt;/gt; - Butang Sembunyikan/Tunjukkan - Klik Kiri atau Tekan pada Segitiga - Keluar Manual - Klik pada Esc atau Tekan pada PS - - - - - Gagal memuatkan "{}". -Semak vita3k.log untuk melihat output konsol untuk mendapatkan butiran. -1. Adakah awak telah memasang perisian tegar? -2. Buat PS Vita apl/gim awak sendiri dan pasang pada Vita3K. -3. Jika anda ingin memasang atau boot Vitamin, ia tidak disokong. - - - - Tindanan Gamepad - Tampilkan Tindanan gamepad digim - HideSembunyikan Tindanan Gamepad - Ubah suai Tindanan Gamepad - Skala Tindanan - Tingkatan Kegelapan - Tetapkan semula Gamepad - ShowTampilkan front/back suis skrin sentuh hadapan/belakang. - Pencetus L2/R2 akan dipaparkan hanya jika mode PSTV didayakan, - - - - - Lalai - - Cari Tema Tersuai PSVita - - Nama - Pembekal - dikemas kini - Saiz - Versi - ID Kandungan - - Tema ini akan dipadamkan. - - - Imej - Tambah Imej - - - Padamkan Background - Tambahkan Background - - - - - YYYY/MM/DD - DD/MM/YYYY - MM/DD/YYYY - - - Tetapkan 12 Jam - Tetapkan 24 Jam - - - - Bahasa System - - - - Denmark - Jerman - Inggeris (United Kingdom) - Inggeris (United States) - Sepanyol - Perancis - Itali - Belanda - Norway - Poland - Portugis (Brazil) - Portugis (Portugal) - Rusia - Finland - Sweden - Turki - - - - - - - - - Mode Modul - Senarai Modul - Pilih modul yang awak inginkan. - Modul Carian - Senarai Jelas - Tiada modul hadir. -Sila muat turun dan pasang PS Vita yang terakhir firmware. - Muat Semula Senarai - - - Dayakan pengoptimuman - Tandai kotak untuk mendayakan pengoptimuman CPU JIT tambahan. - - - Tetapkan semula - Penyampai Bahagian Belakang - Backend renderer pilih pemapar backend pilihan awak. - GPU (Reboot untuk memohon) - Pilih GPU Vita3K yang sepatutnya dijalankan. - Tambah custom driver - Alih keluar custom driver - tinggi - Ketepatan Penyampai - Melumpuhkan V-Sync boleh membetulkan isu kelajuan dalam sesetengah permainan. -Adalah disyorkan untuk memastikan ia didayakan untuk mengelakkan koyakan visual. - Lumpuhkan Sunting Penyegerakan - Penggodam kelajuan, tandai kotak untuk melumpuhkan penyegerakan permukaan antara CPU dan GPU. -Penyegerakan permukaan diperlukan oleh sesetengah permainan. -Memberi rangsangan prestasi yang besar jika dilumpuhkan (khususnya apabila peningkatan dihidupkan). - Terhampir - Dwilinear - Bikubik - Penapis Skrin - Tetapkan penapis pasca pemprosesan untuk digunakan. - Peningkatan Resolusi Dalaman - Dayakan peningkatan untuk Vita3K. -Percubaan: permainan tidak dijamin untuk dipaparkan dengan betul pada lebih daripada 1x. - Anisotropik Penapisan - Penapisan anisotropik ialah teknik untuk meningkatkan kualiti imej permukaan -yang condong berbanding dengan penonton. -Ia tidak mempunyai kelemahan tetapi boleh menjejaskan prestasi. - Penggantian Tekstur - Eksport Tekstur - Import Tekstur - Format Pengeksportan Tekstur - Godam permainan yang membenarkan beberapa permainan berjalan pada 30 FPS berjalan pada 60 FPS pada emulator. -Ambil perhatian bahawa ini adalah penggodaman dan hanya akan berfungsi pada beberapa permainan. -Pada permainan lain, ia mungkin tidak memberi kesan atau membuatkannya berjalan dua kali lebih pantas. - Dilumpuhkan - Penampan berganda - Hos luaran - Tabel halaman - Pemetaan memory - Pemetaan memori boleh meningkatkan prestasi. -mengurangkan penggunaan memori dan kebanyakannya membetulkan pepijat grafik. -Selain itu, banyak peranti tidak begitu stabil pada CPU. - Dayakan Mode Turbo - Menyediakan cara untuk memaksa GPU berjalan pada jam maksimum yang mungkin (kekangan terma masih akan digunakan). - Shader - Gunakan Shader Cache - Tandai kotak untuk mendayakan cache shader untuk menyusunnya terlebih dahulu pada permulaan permainan. -Nyahtanda untuk melumpuhkan ciri ini. - Gunakan Shader Spir-V (ditamatkan) - Hantarkan shader Spir-V yang dijana terus kepada pemandu. -Ambil perhatian bahawa beberapa sambungan berfaedah akan dilumpuhkan, -dan tidak semua GPU serasi dengan ini. - Kemaskan Cache dan Log Shader - - - - Masukkan tugasan butang -Pilih butang 'Enter' awak. - Ini ialah butang yang digunakan sebagai 'Sahkan' dalam dialog aplikasi. -Sesetengah aplikasi tidak menggunakan ini dan mendapatkan butang pengesahan lalai. - O - X - Tandai kotak untuk mendayakan mode PS TV Emulated. - Tandai kotak untuk mendayakan mode Show. - Tandai kotak untuk mendayakan mode Demo. - - - Boot apl dalam skrin penuh - Jejak - Amaran - ralat - kritikal - Dipadamkan - Log Tahap - Pilih tahap log pilihan awak. - Log Arkib - Tandai kotak untuk mendayakan Log Arkib. - Membolehkan Discord Rich Presence untuk menunjukkan aplikasi yang anda jalankan pada Discord. - Textur Cache - Nyahtanda kotak untuk melumpuhkan cache tekstur. - Tunjukkan Compile Shader - Nyahtanda kotak untuk melumpuhkan paparan dialog penyusun shader. - Tunjukkan Kursor Pad Sentuh - Nyahtanda kotak untuk melumpuhkan menunjukkan kursor pad sentuh pada skrin. - Amaran Keserasian Log - Tandai kotak untuk mendayakan amaran keserasian log mengenai isu GitHub. - Menyemak kemas kini - Semak kemas kini secara automatik semasa permulaan. - Tindanan prestasi - Paparkan maklumat prestasi pada skrin sebagai tindanan. - Rendah - Sederhana - Butiran - Pilih butiran tindanan prestasi pilihan awak. - Diatas Kiri - Diatas Pusat - Dibawah Kanan - Dibawah Kiri - Dibawah Pusat - Dibawah Kanan - Kedudukan - Pilih kedudukan tindanan prestasi pilihan awak. - Semak untuk mendayakan pencarian laluan tidak sensitif huruf besar-besaran pada sistem fail sensitif huruf besar-besaran. -TETAP SEMULA PADA MULA SEMULA - Membenarkan emulator cuba mencari fail tanpa mengira kes -pada platform bukan Windows. - Folder Storan Sistem Ditiru - Laluan emulator semasa: - Tukar Laluan Emulator - Tukar laluan folder emulator Vita3K. -Anda perlu mengalihkan folder lama anda ke lokasi baharu secara manual. - Tetapkan Semula Laluan Emulator - Tetapkan semula laluan emulator Vita3K kepada lalai. -Awak perlu mengalihkan folder lama awak ke lokasi baharu secara manual. - Tetapan Konfigurasi Tersuai - Bersihkan Konfigurasi Tersuai - - - GUI Yang Boleh Dilihat - Tandai kotak untuk menunjukkan GUI selepas boot apl. - Bar Maklumat Kelihatan - Tandai kotak untuk menunjukkan bar maklumat dalam pemilih apl. - Bahasa GUI - Pilih bahasa user awak. - Paparkan Mesej Maklumat - Nyahtanda kotak untuk memaparkan mesej maklumat dalam log sahaja. - Paparan Apl Sistem - Nyahtanda kotak untuk melumpuhkan paparan apl sistem pada skrin utama. -Ia akan ditunjukkan dalam bar menu utama sahaja. - Live Area Skrin Apl - Tandai kotak untuk membuka Kawasan Live secara lalai apabila mengklik pada aplikasi. -Jika dilumpuhkan, klik kanan pada aplikasi untuk membukanya. - Regangkan Kawasan Paparan - Tandai kotak untuk membesarkan kawasan paparan agar sesuai dengan saiz skrin. - Tandai kotak untuk menetapkan senarai apl kepada mode grid. - Saiz Ikon Apl - Pilih saiz ikon pilihan awak. - Sokongan fon - Tandai kotak ini untuk mendayakan sokongan fon untuk bahasa Cina dan Korea. -Mendayakan ini akan menggunakan lebih banyak memori dan memerlukan anda memulakan semula emulator. - Firmware font package diperlukan untuk sesetengah aplikasi -dan juga untuk sokongan fon serantau Asia dalam GUI. -Ia juga secara amnya disyorkan untuk GUI. - Tema & Background - ID kandungan tema semasa: - Tetapkan Semula Tema Lalai - Menggunakan tema background - Background User Bersih - Background permulaan semasa: - Tetapkan Semula Background Mula - Background Alpha - Pilih ketelusan latar belakang pilihan awak. -Minimum adalah legap dan maksimum adalah telus. - Kelewatan untuk background - Pilih kelewatan (dalam saat) sebelum menukar background. - Kelewatan untuk skrin mula - Pilih kelewatan (dalam saat) sebelum kembali ke skrin mula. - - - Masuk ke PSN - Jika disemak, permainan akan menganggap user disambungkan ke rangkaian PSN (tetapi di luar talian). - Dayakan HTTP - Tandai kotak ini untuk membolehkan permainan menggunakan protokol HTTP di Internet. - Percubaan Tamat Masa HTTP - Berapa banyak percubaan yang perlu dilakukan apabila pelayan tidak bertindak balas. -Mungkin berguna jika awak mempunyai internet yang sangat tidak stabil atau SANGAT LAMBAT. - HTTP Tamat Masa Tidur - Cuba masa tidur apabila pelayan tidak menjawab. -Mungkin berguna jika awak mempunyai internet yang sangat tidak stabil atau SANGAT LAMBAT. - Percubaan Akhir Baca HTTP - Berapa banyak percubaan yang perlu dilakukan apabila tiada lagi data untuk dibaca, -lebih rendah boleh meningkatkan prestasi tetapi boleh membuat permainan tidak stabil jika anda mempunyai internet yang teruk. - HTTP Baca Akhir Tidur - Cuba masa tidur apabila tiada lagi data untuk dibaca, -lebih rendah boleh meningkatkan prestasi tetapi boleh membuat permainan tidak stabil jika awak mempunyai internet yang teruk. - - - Pembalakan Import - Simbol import modul log. - Pengelogan Eksport - Simbol eksport modul log. - Pembalakan Shader - Lorek log digunakan pada setiap panggilan cabutan. - Pembalakan Seragam - Log shaders nama seragam dan nilai. - Simpan Edit Warna - Simpan permukaan warna ke fail. - Lambakan ELF - Buang kod yang dimuatkan sebagai ELF. - Lapisan Pengesahan (Reboot diperlukan) - Dayakan lapisan pengesahan Vulkan. - Kod Nyah Tonton - Kod Nyahlihat - Nyahlihat Memori - Tonton Memori - Nyahlihat Panggilan Import - Tonton Panggilan Import - - Simpan & Reboot - Simpan & Apply - Simpan & Tutup - Klik pada simpan untuk menyimpan perubahan awak. - - - - Padamkan Piala - Maklumat trofi yang disimpan pada user ini akan dipadamkan. - Terkunci -
Butiran
- Diperolehi - Nama - Tiada trofi. -Awak boleh memperoleh trofi dengan menggunakan aplikasi yang menyokong ciri trofi. - Tak Diperolehi - Asal - Isih - Trofi - Gred - Kemajuan - Dikemas Kini -
- - - Pilih User - Buat User - User berikut telah dibuat User berikut telah dibuat. - Sunting User - Buka User Folder - Nama ini sudah digunakan. - Padamkan User - Pilih user yang ingin awak padamkan. - User berikut akan dipadamkan. - Jika awak memadamkan user, data yang disimpan user itu, trofi akan dipadamkan. - User akan dipadamkan. -Adakah awak pasti mahu meneruskan? - User dipadamkan. - Pilih Avatar - Tetapkan Semula Avatar - Sahkan - User Login Automatik - - - - Versi baharu Vita3K tersedia. - Belakang - Adakah awak mahu membatalkan kemas kini? -Jika anda membatalkan, pada kali anda mengemas kini, Vita3K akan mula memuat turun dari titik ini. - Downloading... -Selepas muat turun selesai, Vita3K akan dimulakan semula secara automatik dan kemudian memasang kemas kini baharu. - Tidak dapat melengkapkan kemas kini. - {} Minit lagi - Seterusnya - {} Detik Tinggal - Adakah awak ingin mengemas kini Vita3K? - Versi terbaru Vita3K telah diinstal. - Versi terbaru Vita3K telah pun diinstal. - Ciri Baharu dalam Versi {} - Pengarang - Komen - Kemas kini - Versi {} - - - - Emulator Vita3K PlayStation Vita - Vita3K ialah emulator PlayStation Vita sumber terbuka yang ditulis dalam C++ untuk Windows, Linux, macOS dan Android. - Emulator masih dalam peringkat pembangunannya jadi sebarang maklum balas dan ujian amat dihargai. - Panduan komprehensif tentang cara menyediakan Vita3K boleh didapati di - Permulaan pantas - muka surat. - Rujuk permainan Komersial dan senarai keserasian Homebrew untuk melihat perkara yang sedang berjalan. - Senarai Keserasian Komersial - Homebrew Senarai Keserasian - Sumbangan dialu-alukan! - Sokongan tambahan boleh didapati dalam saluran #help - Vita3K tidak membenarkan cetak rompak. Awak mesti membuang permainan awak sendiri. - Tunjukkan lainKali - -
diff --git a/lang/user/ua.xml b/lang/user/ua.xml deleted file mode 100644 index 356bb1650..000000000 --- a/lang/user/ua.xml +++ /dev/null @@ -1,865 +0,0 @@ - - - - - - Відкрити бажаний шлях - Відкрити шлях текстур - Завантажити прошивку - Завантажити .pkg - Завантажити .zip, .vpk - Завантажити ліцензію - Вихід - - - Останні використані програми - - - Потоки - Семафори - М'ютекси - Легковагові м'ютекси - Умовні змінні - Легковагові умовні змінні - Маркери події - Розподіл пам'яті - Дизасемблювання - - - Керування користувачами - - - Керування клавіатурою - - - Вітання - - - - - Vita3K: Емулятор PS Vita/PS TV. Перший у світі функціонуючий емулятор PS Vita/PS TV. - Vita3K - це експериментальний емулятор PlayStation Vita/PlayStation TV з відкритим кодом, написаний на C++ для операційних систем Windows, Linux, macOS та Android. - Особлива подяка: Піктограма Vita3K була розроблена: - Якщо ви бажаєте зробити свій внесок, перегляньте наш: - Відвідайте нашу веб-сторінку, аби дізнатися більше: - Якщо ви хочете підтримати нас, то можете задонатити або підписатися на наш: - Команда Vita3K - Розробники - Вкладники - Спонсори - - - - - Перевірити стан програми - Скопіювати звіт Vita3K - Відкрити звіт про стан - Створити звіт про стан - Оновити базу даних - - - Ім'я та ідентифікатор - Звіт роботи програми - - - Створити - Редагувати - Вилучити - - - Відкрити папку - Завантажуваний вміст - Ліцензія - Кеш шейдерів - Журнал шейдерів - - - Довідка - Оновити - - - Ця програма та всі пов'язані дані, враховуючи збережені дані, будуть видалені. - Видалення програми може зайняти деякий час, -в залежності від того, скільки місця вона займає на пристрої. - Ви хочете видалити дані цього доповнення? - Ви хочете видалити цю ліцензію? - - Оновити історію - Версія {} - - Допущено - Не допущено - Рівень {} - Ім'я - Отримання трофеїв - Батьківський контроль - Оновлено - Розмір - Версія - Ідентифікатор - Каталог - Востаннє запущено - Часу проведено - Жодного разу - - - {}секунд - {}хвилин:{}секунд - {}годин:{}хвилин:{}секунд - {}днів:{}годин:{}хвилин:{}секунд - {}тижнів:{}днів:{}годин:{}хвилин:{}секунд - - - - - Сталася помилка. -Код помилки: {} - Відміна - Закрити - Не вдалося завантажити файл. - Не вдалося зберегти файл. - Видалити - Помилка - Файл пошкоджено. - Увімкніть мікрофон. - Ні - Добре - Будь ласка, зачекайте... - Пошук - Обрати все - - Підтвердити - Так - - Неділя - Понеділок - Вівторок - Середа - Четверг - П'ятниця - Субота - - - Січень - Лютий - Березень - Квітень - Травень - Червень - Липень - Серпень - Вересень - Жовтень - Листопад - Грудень - - - Січень - Лютий - Березень - Квітень - Травень - Червень - Липень - Серпень - Вересень - Жовтень - Листопад - Грудень - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - - Прихований трофей - Годину тому - Хвилину тому - Бронза - Золото - Платина - Срібло - {} Годин тому - {} Хвилин тому - - - - - Невідомо - Не запускається - Запускається - Заставка - Меню - У грі - - У грі + - Можливо грати - - - - - Не вдалося під'єднатися до бази даних сумісностей, перевірте доступ до брендмауеру/Інтернету, та спробуйте ще раз пізніше. - Не вдалося завантажити базу даних сумісностей програм, що була оновлена у: {}, спробуйте ще раз пізніше. - Не вдалося завантажити базу даних сумісностей програм, що була завантажена та оновлена у: {}. - База даних суміснотей була успішно оновлена з {} до {}. - -{} Нову програму/програми виявлено, загалом налічується {}! - База даних суміснотей була успішно оновлена з {} до {}. - -{} програм виявлено! - База даних сумісностей, що була оновлена у {} успішно завантажена. - -{} програм виявлено! - - - - Компіляція шейдерів - {} Конвеєрів скомпільовано - {} Шейдерів скомпільовано - - - - - Обрані програми та всі пов'язані дані, разом зі збереженими даними, будуть видалені. - Дані відсутні. - - - Обрані збережені дані буде видалено. - Збережені дані не виявлено. - - Тема - Вільного місця - Очистити все - - - - {} Контролерів під'єднано - Кількість - Прив'язка елементів керування - Колір світлодіоду - Використати власний колір - Оберіть цей пункт, аби використати власний колір світлодіоду контролера. - Червоний - Зелений - Синій - Сумісні контролери не підключені. -Будь ласка, підключіть контролер, сумісний з SDL3. - Скинути налаштування елементів керування контролера - - - - - Прив'язана кнопка - Лівий стік догори - Лівий стік донизу - Лівий стік праворуч - Лівий стік ліворуч - Правий стік догори - Правий стік донизу - Правий стік праворуч - Правий стік ліворуч - D-pad догори - D-pad донизу - D-pad праворуч - D-pad ліворуч - Квадрат - Хрестик - Кружечок - Трикутник - Кнопка Start - Кнопка Select - Кнопка PS - Кнопка L1 - Кнопка R1 - Лише у режимі PS TV. - Кнопка L2 - Кнопка R2 - Кнопка L3 - Кнопка R3 - Інтерфейс - Повний екран - Перемикання сенсора - Перемикання між заднім сенсором та сенсорним екраном. - Перемикання видимості інтерфейсу - Перемикає видимість інтерфейсу у верхній частині екрану коли програма функціонує. - Інше - Перемикання заміни текстур - Зробити знімок екрану - Кнопка вже прив'язана або її неможливо прив'язати. - - - - - Підготовка до запуску програми... - - - - Ви хочете відмінити видалення? - Видалення завершене. - Ви хочете видалити ці збережені дані? - - -
Деталі
- Оновлено -
- - Ви хочете відмінити завантаження? - Збережені дані не виявлені. - Завантаження завершене. - Завантаження... - Ви хочете завантажити ці збережені дані? - - - Ви хочете відмінити збереження? - Не вдалося зберегти файл. -Недостатньо вільного місця на карті пам'яті. - Недостатньо вільного місця на карті пам'яті. - Нові збережені дані - Збереження завершене. - Ви хочете зберегти дані? - Збереження... - Збереження... -Не вимикайте систему та не закривайте програму. - Ви хочете перезаписати ці збережені дані? - -
-
- - - Ця програма буде закрита. - Дані, що будуть видалені: - - - - Фільтр - Сортувати програми за - Все - За регіоном - США - Європа - Японія - Азія - За типом - Комерційне - Homebrew - За статусом сумісності - Версія - Каталог - Сумісність - Останній запуск - Оновити - - - - Програму було додано на головний екран. - Видалити все - Сповіщення будуть видалені. - Не вдалося завантажити. - Завантаження завершене. - Завантаження... - Сповіщень немає. - Ви отримали трофей! - - - - - Завантаження прошивки - Завантаження триває, будь ласка, зачекайте... - Прошивку успішно завантажено. - Версія прошивки: - Пакет шрифтів прошивки відсутній, будь ласка, завантажте його. - Пакет шрифтів прошивки потрібен для деяких програм, -а також для підтримки шрифтів азійського регіону. (загалом рекомендовано) - Видалити файл завантаження прошивки? - - - Обрати тип ліцензії - Обрати work.bin/rif - Ввести zRIF - Ввести ключ zRIF - Будь ласка, введіть свій zRIF тут - Ctrl + C аби скопіювати, Ctrl + V аби вставити. - Видалити файл pkg? - Видалити файл work.bin/rif? - Не вдалося завантажити пакет. -Будь ласка, перевірте файли pkg та work.bin/rif або ключ zRIF. - - - Оберіть тип завантаження - Оберіть файл - Оберіть директорію - {} архівів з сумісним вмістом було знайдено. - {} архівів успішно завантажено: - Оновити програму до: - Не вдалося завантажити вміст {} архівів: - Не знайдено сумісного вмісту у {} архівах: - Видалити архів? - - - Ліцензію успішно завантажено. - Не вдалося завантажити ліцензію. -Будь ласка, перевірте файл work.bin/rif або ключ zRIF. - - - Перевстановити цей вміст? - Цей вміст вже встановлено. - Чи бажаєте ви перевстановити його та перезаписати існуючі дані? - - - - - Запуск - Продовжити - - Використовуючи конфігурацію для клавіатури в налаштуваннях керування - Прошивку не виявлено. Настійно рекомендується завантаження - Шрифт прошивки не виявлено. Завантаження рекомендується для шрифта тексту Live Area - Довідка Live Area - Перегляд списку програм - D-pad, лівий стік, коліщатко догори/донизу або використовуючи повзунок - Запуск програми - натисніть на Start або на хрестик - Показати/приховати Live Area під час роботи програми - Натиснути PS - Вийти з Live Area - Натиснути Esc або кружечок - Довідка по інструкції - Перегляд сторінки - D-pad, лівий стік, коліщатко догори/донизу, використовуючи повзунок або настиснути </> - Приховати/показати кнопку - Натиснути лівий стік або трикутник - закрити довідку - Натиснути Esc або PS - - - - - - За замовчуванням - - Знайти користувацькі теми для PSVita - - Назва - Постачальник - Оновлено - Розмір - Версія - Ідентифікатор вмісту - - Цю тему буде видалено. - - - Зображення - Додати зображення - - - Видалити фон - Додати фон - - - - - РРРР/ММ/ДД - ДД/ММ/РРРР - ММ/ДД/РРРР - - - 12-годинний формат - 24-годинний формат - - - - Мова системи - - - - Данська - Німецька - Англійська (Сполучене Королівство) - Англійська (Сполучені Штати) - Іспанська - Французька - Італійська - Нідерландська - Норвезька - Польська - Португальська (Бразилія) - Португальська (Португалія) - Російська - Фінська - Шведська - Турецька - - - - - - - - - Режим модулів - Список модулів - Оберіть бажані модулі. - Пошук модулів - Очистити список - Модулі відсутні. -Будь ласка, завантажте останню прошивку PS Vita. - Оновити список - - - Дозволити оптимізації - Оберіть цей пункт, аби дозволити додаткові JIT-оптимізації процесора. - - - Скинути - Рендер бекенду - Оберіть бажаний рендер бекенду. - Графічний процесор (перезавантажте систему, аби застосувати) - Оберіть графічний процесор, на якому буде запускатися Vita3K. - Стандарт - Високо - Точність рендеру - Вертикальна синхронізація - Вимикання вертикальної синхронізації може покращити швидкодію деяких ігор. -Рекомендується увімкнути цей параметр, аби уникнути розривів зображення. - Вимкнути синхронізацію поверхні - Оберіть цей пункт, аби вимкнути сихронізацію поверхонь центрального та графічного процесорів. -Синхронізація поверхні необхідна для деяких ігор. -Суттєво збільшує продуктивність, якщо вимкнена (особливо якщо ввімкнено апскейлінг). - Асинхронна компіляція конвеєрів - Дозволяє конвеєрам компілюватись одночасно на декількох потоках. -Це покращує якість компіляції за рахунок тимчасових графічних збоїв. - Поблизу - Білінійно - Бікубічно - Фільтр екрану - Увімкніть фільтр постобробки, аби застосувати. - Покращення внутрішньої роздільності - Увімкнути покращення внутрішньої роздільності на Vita3K. -Експериментально: правильний рендеринг ігор на роздільності більшій, ніж 1х, не гарантовано. - Анізотропна фільтрація - Анізотропна фільтрація - це технологія покращення якості зображення поверхонь, -що нахилені з точки зору глядача. -Технологія не має недоліків, але може вплинути на продуктивність системи. - Заміна текстур - Експорт текстур - Імпорт текстур - Формат екпорту текстур - Шейдери - Використовувати кеш шейдерів - Оберіть цей пункт, аби дозволити кешу компілюватися зазделегідь під час запуску гри. -Приберіть позначку, аби відключити функцію. - Використовувати шейдер Spir-V (застарілий) - Передати отриманий шейдер Spir-V напряму до драйвера. -Зауважте, що деякі корисні розширення будуть відключені, -і не всі графічні процесори сумісні з цим шейдером. - Очистити кеш та журнал шейдерів - Злам FPS - Злам ігор, що дозволяє запускати деякі ігри у 60 FPS замість 30. -Зауважте, що це буде працювати лише у деяких іграх. -У інших іграх злам не матиме ніякого ефекту, або покращить їх швидкодію. - - - - Призначення кнопки Enter -Оберіть кнопку 'Enter'. - Ця кнопка використовується для підтвердження у діалогових вікнах. -У деяких програмах це не працюватиме, натомість буде використовуватись кнопка підтвердження за замовчуванням. - Кружечок - Хрестик - Режим PS TV - Оберіть цей пункт, аби ввімкнути режим емуляції PS TV. - Режим показу - Оберіть цей пункт, аби ввімкнути режим показу. - Демо-режим - Оберіть цей пункт, аби ввімкнути демо-режим. - - - Запускати програми у повноекранному режимі - Трасування - Інформація - Попередження - Помилка - Критична - Вимкнено - Рівень журналу - Оберіть бажаний рівень журналу. - Журнал архіву - Оберіть цей пункт, аби ввімкнути журнал архіву. - Дозволити запущеній програмі відображатися у Discord. - Кеш текстур - Приберіть позначку, аби відключити кеш текстур. - Відображати компіляцію шейдерів - Приберіть позначку, аби приховати вікно компіляції шейдерів. - Відображати курсор тачпаду - Приберіть позначку, аби приховати курсор тачпаду на екрані. - Попередження помилки сумісності журналу - Оберіть цей пункт, аби відображати попередження помилки сумісності журналу. - Перевірити наявність оновлень - Автоматично перевіряти наявність оновлень під час запуску. - Оверлей продуктивності - Відображати інформацію про продуктивність на екранному оверлеї. - Мінімум - Низько - Медіум - Максимум - Деталі - Оберіть бажаний елемент оверлею продуктивності. - Зверху ліворуч - Зверху по центру - Зверху праворуч - Знизу ліворуч - Знизу по центру - Знизу праворуч - Позиція - Обрати бажане місце на екрані для оверлею продуктивності. - Оберіть цей пункт, аби ввімкнути не чутливий до реєстру пошук шляху на чутливих до реєстру файлових системах. -Скидається при перезавантаженні - Дозволяє емулятору шукати файли незалежно від реєстру -на операційних системах окрім Windows. - Папка емульованої системи - Чинний шлях емулятора: - Змінити шлях емулятора - Змінити папку емулятора Vita3K. -Необхідно власноруч перемістити стару папку на нове місце. - Скинути шлях емулятора - Скинути шлях емулятора Vita3K до налаштувань за замовчуванням. -Необхідно власноруч перемістити стару папку на нове місце. - Налаштування користувацької конфігурації - Скинути користувацьку конфігурацію - - - Графічний інтерфейс видимий - Оберіть цей пункт, аби відображати графічний інтерфейс під час запуску програми. - Інформаційна панель видима - Оберіть цей пункт, аби відображати інформаційну панель у селекторі програм. - Мова графічного інтерфейсу - Оберіть мову користувача. - Відображати інформаційні повідомлення - Приберіть позначку, аби відображати інформаційні повідомлення лише у журналі. - Відображати системні програми - Приберіть позначку, аби системні програми не відображалися на головному екрані. -Вони будуть відображатися лише на панелі у головному меню. - Екран програми Live Area - Оберіть цей пункт, аби програма Live Area відкривалася за замовчуванням при натисканні на піктограму. -Якщо програму вимкнено, натисніть на піктограму правим стіком, аби запустити. - Розтягнути зону дисплею - Оберіть цей пункт, аби збільшити зону дисплею, щоб він вмістився у рамки екрану. - Режим сітки - Оберіть цей пункт, аби перенести список програм у режим сітки. - Розмір піктограми програми - Оберіть бажаний розмір піктограми. - Підтримка шрифтів - Азійський регіон - Оберіть цей пункт, аби увімкнути підтримку шрифтів для китайської та корейської мови. -Цей параметр використовуватиме більше пам'яті, і вам знадобиться перезавантажити емулятор. - Прошивка пакету шрифтів необхідна для деяких програм, -підтримки шрифтів азійського регіону і графічного інтерфейсу. -Це загалом рекомендовано для графічного інтерфейсу. - Тема & Фон - Чинний ідентифікатор теми: - Скинути до базової теми - Використання теми/фону - Очистити користувацькі фони - Чинний фон початкового екрану: - Скинути фон початкового екрану - Прозорість фону - Оберіть бажану прозорість фону. -Мінімум - непрозорий, максимум - прозорий. - Затримка фону - Оберіть затримку (у секундах) між зміною фону. - Затримка початкового екрану - Оберіть затримку (у секундах) перш ніж станеться повернення на початковий екран. - - - підключено до PSN - Якщо обрано - гра буде вважати, що користувача підключено до PSN (однак підключення насправді відсутнє). - Дозволити HTTP - Оберіть цей пункт, аби ігри мали змогу використовувати HTTP-протокол інтернету. - Спроби під'єднання HTTP - Скільки разів спробувати під'єднатися, якщо сервер не відповідає. -Може допомогти, якщо ви маєте дуже нестабільний або дуже слабкий інтернет. - Перехід HTTP у режим сну - Спроба увійти в режим сну, якщо сервер не відповідає. -Може допомогти, якщо ви маєте дуже нестабільний або дуже слабкий інтернет. - Спроби прочитати дані протоколу HTTP - Скільки разів спробувати прочитати дані, якщо нових даних немає, -зниження параметру може покращити продуктивність, але ігри можуть працювати нестабільно при слабкому інтернеті. - Режим сну після спроб прочитати дані протоколу HTTP - Спроба увійти у режим сну, якщо нових даних для читання немає, -зниження параметру може покращити продуктивність, але ігри можуть працювати нестабільно при слабкому інтернеті. - - - Журналювання імпорту - Журналювати символи модуля імпорту. - Журналювання експорту - Журналювати символи модуля експорту. - Журналювання шейдерів - Журнальовані шейдери використовуються при кожному виклику відтворення. - Рівномірне журналювання - Рівномірно журналювати назви та значення шейдерів. - Зберегти кольорові поверхні - Зберегти кольорові поверхні до файлів. - Збереження у форматі ELF - Зберегти завантажений код у вигляді ELF-файлів. - Рівень валідації (необхідний перезапуск) - Дозволити рівень валідації технології Vulkan. - Не спостерігати за кодом - Спостерігати за кодом - Не спостерігати за пам'яттю - Спостерігати за пам'яттю - Не спостерігати за імпортованими викликами - Спостерігати за імпортованими викликами - - Зберегти & Перезапустити - Зберегти & Застосувати - Зберегти & Закрити - Натисніть 'Зберегти', аби зберегти зміни. - - - - Видалити трофей - Інформацію про отримання цього трофею користувачем буде видалено. - Заблоковано -
Деталі
- Отримано - Назва - Трофеїв немає. -Ви можете отримати трофеї використовуючи програми, що підтримують функцію трофеїв. - Не отримано - Оригінал - Сортувати - Трофеї - Ступінь - Прогрес - Оновлено -
- - - Обрати користувача - Створити користувача - Користувача було створено. - Редагувати користувача - Відкрити папку користувача - Це ім'я вже використовується. - Видалити користувача - Оберіть, якого користувача ви хочете видалити. - Цього користувача буде видалено. - Якщо ви видалите цього користувача, будуть також видалені його збережені дані та трофеї. - Користувача буде видалено. -Ви впевнені, що хочете продовжити? - Користувача видалено. - Обрати аватар - Скинути аватар - Підтвердити - Автоматичний вхід у систему - - - - Доступна нова версія Vita3K. - Назад - Ви хочете відмінити оновлення? -Якщо ви скасуєте, під час наступного оновлення Vita3K почне завантаження з цього моменту. - Завантаження... -Після завантаження, Vita3K автоматично перезапуститься та встановить нове оновлення. - Не вдалося виконати оновлення. - {} хвилин залишилось - Далі - {} секунд залишилось - Ви хочете оновити Vita3K? - Більш нову версію Vita3K вже встановлено. - Останню версію Vita3K вже встановлено. - Нові функції у версії {} - Автори - Опис - Оновлення - Версія {} - - - - Емулятор PlayStation Vita Vita3K - Vita3K - це емулятор PlayStation Vita з відкритим кодом, написаний на C++ для операційних систем Windows, Linux, macOS та Android. - Емулятор досі у розробці, тому ми були б дуже вдячні за відгуки та тестування. - Встановити прошивку - Завантажити пакет шрифтів прошивки - Повне керівництво з налаштування Vita3K можна побачити - Тут - Сторінка. - Перевірте списки сумісності, аби дізнатися, що нараізі запускається. - Список сумісності комерційних ігор - Список сумісності Homebrew - Будь-який внесок вітається! - Додаткову інформацію можна знайти в каналі #help у - Vita3K не заохочує до піратства. Ви маєте використовувати власні дампи пам'яті ігор. - Відобразити наступного разу - -
diff --git a/tools/i18n/generate_lang_catalog.py b/tools/i18n/generate_lang_catalog.py new file mode 100644 index 000000000..09a726adc --- /dev/null +++ b/tools/i18n/generate_lang_catalog.py @@ -0,0 +1,239 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import re +from typing import Any + + +PLACEHOLDER_RE = re.compile(r"\{[^{}]*\}") +LANG_STRING_RE = re.compile(r'^LANG_STRING\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*"([^"]+)"\s*\)$') +CATALOG_PREFIX = "overlay_" + + +def load_string_entries(path: Path) -> tuple[tuple[str, str], ...]: + entries: list[tuple[str, str]] = [] + + for lineno, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): + line = raw_line.strip() + if not line or line.startswith("//"): + continue + + match = LANG_STRING_RE.fullmatch(line) + if not match: + raise RuntimeError(f"Unsupported entry in {path}:{lineno}: {raw_line}") + + entries.append((match.group(1), match.group(2))) + + if not entries: + raise RuntimeError(f"No string entries found in {path}") + + return tuple(entries) + + +def load_catalog(path: Path) -> dict[str, str]: + raw: dict[str, Any] = json.loads(path.read_text(encoding="utf-8")) + catalog: dict[str, str] = {} + + for key, value in raw.items(): + if isinstance(value, str): + catalog[key] = value + elif isinstance(value, dict) and isinstance(value.get("message"), str): + catalog[key] = value["message"] + else: + raise RuntimeError(f"Unsupported catalog entry in {path}: {key}") + + return catalog + + +def locale_tag_from_path(path: Path) -> str: + stem = path.stem + if not stem.startswith(CATALOG_PREFIX): + raise RuntimeError(f"Catalog filename must start with {CATALOG_PREFIX!r}: {path.name}") + + locale_tag = stem[len(CATALOG_PREFIX):] + if not locale_tag: + raise RuntimeError(f"Catalog filename is missing a locale tag: {path.name}") + + return locale_tag + + +def placeholder_set(text: str) -> tuple[str, ...]: + return tuple(sorted(PLACEHOLDER_RE.findall(text))) + + +def escape_cpp(text: str) -> str: + return ( + text.replace("\\", "\\\\") + .replace('"', '\\"') + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + ) + + +def build_locale( + locale_tag: str, + english: dict[str, str], + translated: dict[str, str], + string_entries: tuple[tuple[str, str], ...], +) -> dict[str, Any]: + unknown = sorted(key for key in translated if key not in english) + if unknown: + raise RuntimeError(f"{locale_tag} has unknown keys: {unknown}") + + strings: list[str] = [] + for _, key in string_entries: + source = english[key] + value = translated.get(key, source) + if placeholder_set(value) != placeholder_set(source): + raise RuntimeError( + f"Placeholder mismatch in {locale_tag}:{key} " + f"{placeholder_set(source)} != {placeholder_set(value)}" + ) + strings.append(value) + + return { + "tag": locale_tag, + "strings": strings, + } + + +def render_string_array(values: list[str], indent: str) -> str: + body = ",\n".join(f'{indent}"{escape_cpp(value)}"' for value in values) + return "{\n" + body + "\n" + indent[:-4] + "}" + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--input-dir", required=True) + parser.add_argument("--strings-def", required=True) + parser.add_argument("--output-header", required=True) + parser.add_argument("--output-source", required=True) + args = parser.parse_args() + + input_dir = Path(args.input_dir) + strings_def = Path(args.strings_def) + output_header = Path(args.output_header) + output_source = Path(args.output_source) + output_header.parent.mkdir(parents=True, exist_ok=True) + output_source.parent.mkdir(parents=True, exist_ok=True) + + string_entries = load_string_entries(strings_def) + expected_keys = tuple(key for _, key in string_entries) + english_path = input_dir / f"{CATALOG_PREFIX}en.json" + if not english_path.is_file(): + raise RuntimeError(f"Missing required English catalog: {english_path}") + + english = load_catalog(english_path) + + missing_in_english = [key for key in expected_keys if key not in english] + if missing_in_english: + raise RuntimeError(f"English catalog is missing keys: {missing_in_english}") + + catalog_paths = sorted(input_dir.glob(f"{CATALOG_PREFIX}*.json")) + if not catalog_paths: + raise RuntimeError(f"No catalogs found in {input_dir} matching {CATALOG_PREFIX}*.json") + + locale_tags = [locale_tag_from_path(path) for path in catalog_paths] + duplicate_tags = sorted({tag for tag in locale_tags if locale_tags.count(tag) > 1}) + if duplicate_tags: + raise RuntimeError(f"Duplicate locale tags found: {duplicate_tags}") + + locales = [ + build_locale(locale_tag_from_path(path), english, load_catalog(path), string_entries) + for path in catalog_paths + ] + locales.sort(key=lambda locale: (locale["tag"] != "en", locale["tag"].lower())) + + string_count = len(string_entries) + + header = f"""// Generated by tools/i18n/generate_lang_catalog.py. Do not edit by hand. +#pragma once + +#include +#include +#include + +namespace lang::generated {{ + +inline constexpr std::size_t k_string_count = {string_count}; + +struct LocaleData {{ + std::string_view tag; + std::array strings; +}}; + +const LocaleData &english_locale(); +const LocaleData *find_locale(std::string_view tag); + +}} // namespace lang::generated +""" + output_header.write_text(header, encoding="utf-8") + + locale_blocks: list[str] = [] + for locale in locales: + locale_blocks.append( + " LocaleData{\n" + f' "{escape_cpp(locale["tag"])}",\n' + f' {render_string_array(locale["strings"], " ")}\n' + " }" + ) + + source = f"""// Generated by tools/i18n/generate_lang_catalog.py. Do not edit by hand. +#include "generated_catalog.h" + +#include +#include +#include + +namespace lang::generated {{ + +namespace {{ + +constexpr LocaleData k_locales[] = {{ +{",\n".join(locale_blocks)} +}}; + +bool locale_equals(std::string_view lhs, std::string_view rhs) {{ + if (lhs.size() != rhs.size()) + return false; + + for (std::size_t index = 0; index < lhs.size(); ++index) {{ + const char left = lhs[index] == '_' ? '-' : static_cast(std::tolower(static_cast(lhs[index]))); + const char right = rhs[index] == '_' ? '-' : static_cast(std::tolower(static_cast(rhs[index]))); + if (left != right) + return false; + }} + + return true; +}} + +}} // namespace + +const LocaleData &english_locale() {{ + const auto *english = find_locale("en"); + if (!english) {{ + LOG_ERROR_ONCE("English locale is missing from the generated catalog. Falling back to the first available locale."); + return k_locales[0]; + }} + + return *english; +}} + +const LocaleData *find_locale(std::string_view tag) {{ + const auto *match = std::find_if(std::begin(k_locales), std::end(k_locales), [tag](const LocaleData &locale) {{ + return locale_equals(locale.tag, tag); + }}); + + return (match == std::end(k_locales)) ? nullptr : match; +}} + +}} // namespace lang::generated +""" + output_source.write_text(source, encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/vita3k/CMakeLists.txt b/vita3k/CMakeLists.txt index e8aa40a75..b4ec49ed3 100644 --- a/vita3k/CMakeLists.txt +++ b/vita3k/CMakeLists.txt @@ -38,6 +38,8 @@ execute_process(COMMAND ${CMD} OUTPUT_VARIABLE GIT_REPO OUTPUT_STRIP_TRAILING_WHITESPACE) +set(VITA3K_OFFICIAL_UPDATE_BUILD 0) + if (NOT GIT_REPO STREQUAL "") string(FIND ${GIT_REPO} "git@github.com:" IS_GIT_SSH) if (IS_GIT_SSH EQUAL 0) @@ -54,10 +56,7 @@ if(GIT_HASH STREQUAL "") set(GIT_COUNT 0) elseif((GIT_REPO STREQUAL "Vita3K") AND ((GIT_BRANCH STREQUAL "master") OR (GIT_BRANCH STREQUAL "HEAD"))) set(VITA3K_GIT_REV "${GIT_HASH}") - - if(USE_VITA3K_UPDATE) - add_definitions(-DUSE_VITA3K_UPDATE) - endif() + set(VITA3K_OFFICIAL_UPDATE_BUILD 1) else() set(VITA3K_GIT_REV "${GIT_HASH}-${GIT_REPO}/${GIT_BRANCH}") endif() @@ -73,6 +72,19 @@ include_directories(${CMAKE_BINARY_DIR}/vita3k/config) if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") set(LINUX TRUE) + + find_package(X11) + if(X11_FOUND) + add_compile_definitions(HAVE_X11) + endif() + + find_package(PkgConfig) + if(PkgConfig_FOUND) + pkg_check_modules(WAYLAND wayland-client) + if(WAYLAND_FOUND) + add_compile_definitions(HAVE_WAYLAND) + endif() + endif() endif() get_boost() @@ -90,10 +102,14 @@ if(NOT MSVC) add_compile_options(-Wformat -Werror=format-security) endif() +if(NOT ANDROID) + include(${CMAKE_SOURCE_DIR}/cmake/qt6.cmake) +endif() + add_subdirectory(app) add_subdirectory(audio) -add_subdirectory(bgm_player) add_subdirectory(camera) +add_subdirectory(input) add_subdirectory(config) add_subdirectory(cpu) add_subdirectory(ctrl) @@ -103,11 +119,12 @@ add_subdirectory(dialog) add_subdirectory(display) add_subdirectory(features) add_subdirectory(glutil) -add_subdirectory(gui) +add_subdirectory(updater) +if(NOT ANDROID) + add_subdirectory(gui-qt) +endif() add_subdirectory(gxm) -add_subdirectory(host) add_subdirectory(ime) -add_subdirectory(lang) add_subdirectory(net) add_subdirectory(ngs) add_subdirectory(np) @@ -115,12 +132,14 @@ add_subdirectory(emuenv) add_subdirectory(http) add_subdirectory(io) add_subdirectory(kernel) +add_subdirectory(lang) add_subdirectory(mem) add_subdirectory(patch) add_subdirectory(module) add_subdirectory(modules) add_subdirectory(motion) add_subdirectory(nids) +add_subdirectory(overlay) add_subdirectory(regmgr) add_subdirectory(renderer) add_subdirectory(rtc) @@ -133,8 +152,24 @@ add_subdirectory(packages) add_subdirectory(vkutil) if(ANDROID) - add_library(vita3k SHARED main.cpp interface.cpp performance.cpp) - target_link_libraries(vita3k PRIVATE SDL3::SDL3 android) + add_library(vita3k SHARED + android/jni/android_state.cpp + android/jni/filesystem_android.cpp + android/jni/main_android.cpp + android/jni/native_bootstrap.cpp + android/jni/native_config.cpp + android/jni/native_drivers.cpp + android/jni/native_users.cpp + android/jni/native_apps.cpp + android/jni/native_install.cpp + android/jni/native_session.cpp + android/jni/input_overlay.cpp + android/jni/ime.cpp + interface.cpp + performance.cpp + ) + target_include_directories(vita3k PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(vita3k PRIVATE SDL3::SDL3 pugixml::pugixml android) if(CMAKE_BUILD_TYPE STREQUAL "Release") # we need to exclude all symbols except SDL @@ -173,7 +208,10 @@ if(WIN32) target_sources(vita3k PRIVATE util/src/vc_runtime_checker.cpp) endif() -target_link_libraries(vita3k PRIVATE app audio bgm_player config cppcommon ctrl display gdbstub gui gxm io miniz modules motion packages patch renderer shader touch util) +target_link_libraries(vita3k PRIVATE app audio compat config cppcommon ctrl dialog display ime lang gdbstub gxm io miniz modules motion packages patch renderer shader touch util psvpfsparser) +if(NOT ANDROID) + target_link_libraries(vita3k PRIVATE gui-qt) +endif() if(USE_DISCORD_RICH_PRESENCE) target_link_libraries(vita3k PRIVATE discord-rpc) endif() @@ -182,10 +220,47 @@ if(LINUX) target_link_libraries(vita3k PRIVATE -static-libgcc -static-libstdc++) endif() -set_target_properties(vita3k PROPERTIES OUTPUT_NAME Vita3K - ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +if(NOT ANDROID) + file(GLOB VITA3K_QT_TS_FILES CONFIGURE_DEPENDS + "${CMAKE_SOURCE_DIR}/i18n/qt/*.ts") + set(VITA3K_QT_QM_DIR "${CMAKE_BINARY_DIR}/qt_translations") + set(VITA3K_QT_QM_FILES "") + + foreach(ts_file IN LISTS VITA3K_QT_TS_FILES) + get_filename_component(ts_name "${ts_file}" NAME_WE) + set(qm_file "${VITA3K_QT_QM_DIR}/${ts_name}.qm") + list(APPEND VITA3K_QT_QM_FILES "${qm_file}") + add_custom_command( + OUTPUT "${qm_file}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${VITA3K_QT_QM_DIR}" + COMMAND $ "${ts_file}" -qm "${qm_file}" + DEPENDS "${ts_file}" + VERBATIM) + endforeach() + + add_custom_target(qt_app_translations DEPENDS ${VITA3K_QT_QM_FILES}) + add_dependencies(vita3k qt_app_translations) + + add_custom_target(update_qt_translations + COMMAND $ + -no-obsolete + -recursive + "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/gui-qt/src" + "${CMAKE_CURRENT_SOURCE_DIR}/gui-qt/include" + -ts "${CMAKE_SOURCE_DIR}/i18n/qt/vita3k_en.ts" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Updating Qt translation source file") +endif() + +set_target_properties(vita3k PROPERTIES OUTPUT_NAME Vita3K) + +if(NOT ANDROID) + set_target_properties(vita3k PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() if(APPLE) add_custom_command( @@ -210,15 +285,8 @@ if(APPLE) TARGET vita3k POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "$/../Resources/data" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../lang" "$/../Resources/lang" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${VITA3K_QT_QM_DIR}" "$/../Resources/translations" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin/" "$/../Resources/shaders-builtin") - if(USE_VITA3K_UPDATE) - add_custom_command( - TARGET vita3k - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/script/update-macos.sh" "$/../Resources/update-vita3k.sh") - endif() - if(USE_DISCORD_RICH_PRESENCE) if(ARCHITECTURE STREQUAL "x86_64") set(discord_dylib "${CMAKE_BINARY_DIR}/external/discord_game_sdk/lib/x86_64/discord_game_sdk.dylib") @@ -242,11 +310,17 @@ if(APPLE) message(STATUS "MoltenVK dylib path: ${MOLTENVK_DYLIB}") target_sources(vita3k PRIVATE ${MOLTENVK_DYLIB}) set_source_files_properties(${MOLTENVK_DYLIB} PROPERTIES MACOSX_PACKAGE_LOCATION Frameworks) - + + qt_finalize_target(vita3k) + if(MACDEPLOYQT_EXECUTABLE) + add_custom_command(TARGET vita3k POST_BUILD + COMMAND "${MACDEPLOYQT_EXECUTABLE}" "$/../.." "$<$:-no-strip>") + endif() + add_custom_command( TARGET vita3k POST_BUILD - COMMAND codesign --force --deep --preserve-metadata=entitlements,requirements,flags,runtime --sign - $/Vita3K) + COMMAND codesign --force --deep --preserve-metadata=entitlements,requirements,flags,runtime --sign - "$/../..") elseif(LINUX) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath='$ORIGIN'") if(BUILD_APPIMAGE) @@ -256,13 +330,9 @@ elseif(LINUX) POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "${APPDIR}/usr/share/Vita3K" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "${APPDIR}/usr/share/Vita3K/data" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../lang" "${APPDIR}/usr/share/Vita3K/lang" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "${APPDIR}/usr/share/Vita3K/shaders-builtin") - if(USE_VITA3K_UPDATE) - set(LINUXDEPLOY_WRAPPER "${CMAKE_SOURCE_DIR}/appimage/build_updater.sh") - else() - set(LINUXDEPLOY_WRAPPER "${CMAKE_SOURCE_DIR}/appimage/build.sh") - endif() + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "${APPDIR}/usr/share/Vita3K/shaders-builtin" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "${APPDIR}/usr/share/Vita3K/icons") + set(LINUXDEPLOY_WRAPPER "${CMAKE_SOURCE_DIR}/appimage/build.sh") if(USE_DISCORD_RICH_PRESENCE) set(DISCORD_LIB_APPIMAGE "-l \"${CMAKE_BINARY_DIR}/external/discord_game_sdk/lib/x86_64/libdiscord_game_sdk.so\"") else() @@ -274,7 +344,7 @@ elseif(LINUX) COMMAND ${LINUXDEPLOY_WRAPPER} ${LINUXDEPLOY_COMMAND} --appdir="${APPDIR}" -e "$" --icon-filename="vita3k" -i "${CMAKE_CURRENT_SOURCE_DIR}/../data/image/icon.png" -d "${CMAKE_CURRENT_SOURCE_DIR}/../appimage/vita3k.desktop" - --custom-apprun="${CMAKE_CURRENT_SOURCE_DIR}/../appimage/apprun.sh" --output=appimage ${DISCORD_LIB_APPIMAGE} + --custom-apprun="${CMAKE_CURRENT_SOURCE_DIR}/../appimage/apprun.sh" --plugin qt --output=appimage ${DISCORD_LIB_APPIMAGE} COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_BINARY_DIR}/*.AppImage*" "$/" COMMAND ${CMAKE_COMMAND} -E rm "${CMAKE_CURRENT_BINARY_DIR}/*.AppImage*") endif() @@ -282,14 +352,9 @@ elseif(LINUX) TARGET vita3k POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "$/data" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../lang" "$/lang" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "$/shaders-builtin") - if(USE_VITA3K_UPDATE) - add_custom_command( - TARGET vita3k - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/script/update-linux.sh" "$/update-vita3k.sh") - endif() + COMMAND ${CMAKE_COMMAND} -E copy_directory "${VITA3K_QT_QM_DIR}" "$/translations" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "$/shaders-builtin" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "$/icons") if(USE_DISCORD_RICH_PRESENCE) add_custom_command( TARGET vita3k @@ -300,19 +365,14 @@ elseif(WIN32) target_link_libraries(vita3k PRIVATE Version) target_sources(vita3k PRIVATE resource.h Vita3K.ico Vita3K.rc Windows.manifest) set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vita3k) - set_target_properties(vita3k PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/bin/$(Configuration)") + set_target_properties(vita3k PROPERTIES WIN32_EXECUTABLE TRUE VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/bin/$(Configuration)") add_custom_command( TARGET vita3k POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "$/data" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../lang" "$/lang" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "$/shaders-builtin") - if(USE_VITA3K_UPDATE) - add_custom_command( - TARGET vita3k - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/script/update-windows.bat" "$/update-vita3k.bat") - endif() + COMMAND ${CMAKE_COMMAND} -E copy_directory "${VITA3K_QT_QM_DIR}" "$/translations" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "$/shaders-builtin" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "$/icons") if(USE_DISCORD_RICH_PRESENCE) add_custom_command( TARGET vita3k @@ -326,13 +386,21 @@ elseif(WIN32) COMMAND ${CMAKE_COMMAND} -E copy_if_different "${OPENSSL_ROOT_DIR}/bin/libssl-3-x64.dll" "$" COMMAND ${CMAKE_COMMAND} -E copy_if_different "${OPENSSL_ROOT_DIR}/bin/libcrypto-3-x64.dll" "$") endif() + if(WINDEPLOYQT_EXECUTABLE) + add_custom_command(TARGET vita3k POST_BUILD + COMMAND "${WINDEPLOYQT_EXECUTABLE}" --no-compiler-runtime --no-opengl-sw --no-patchqt + --no-system-d3d-compiler --no-system-dxc-compiler + --plugindir "$,$/plugins,$/share/qt6/plugins>" + --translationdir "$,$/translations,$/share/qt6/translations>" + --verbose 0 "$") + endif() elseif(ANDROID) add_custom_command( TARGET vita3k POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "${CMAKE_CURRENT_SOURCE_DIR}/../android/assets/data" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../lang" "${CMAKE_CURRENT_SOURCE_DIR}/../android/assets/lang" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "${CMAKE_CURRENT_SOURCE_DIR}/../android/assets/shaders-builtin" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../data" "${CMAKE_CURRENT_SOURCE_DIR}/../android/app/assets/data" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/shaders-builtin" "${CMAKE_CURRENT_SOURCE_DIR}/../android/app/assets/shaders-builtin" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/icons" "${CMAKE_CURRENT_SOURCE_DIR}/../android/app/assets/icons" ) endif() diff --git a/vita3k/android/jni/android_state.cpp b/vita3k/android/jni/android_state.cpp new file mode 100644 index 000000000..e01dfbc82 --- /dev/null +++ b/vita3k/android/jni/android_state.cpp @@ -0,0 +1,247 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "android_state.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +AndroidSessionState session_state; + +} // namespace + +AndroidSessionState &android_session_state() { + return session_state; +} + +EmuEnvState *get_emuenv() { + return android_session_state().emuenv.get(); +} + +app::AppSessionController *get_app_session_controller() { + return android_session_state().app_session_controller.get(); +} + +Root &get_root_paths() { + return android_session_state().root_paths; +} + +std::string jstring_to_string(JNIEnv *env, jstring str) { + const char *utf = env->GetStringUTFChars(str, nullptr); + std::string result(utf); + env->ReleaseStringUTFChars(str, utf); + return result; +} + +bool is_ime_dialog_active(const EmuEnvState &emuenv) { + return emuenv.common_dialog.type == IME_DIALOG + && emuenv.common_dialog.status == SCE_COMMON_DIALOG_STATUS_RUNNING; +} + +bool is_any_ime_active(const EmuEnvState &emuenv) { + return emuenv.ime.state || is_ime_dialog_active(emuenv); +} + +void finish_ime_dialog(EmuEnvState &emuenv) { + auto &dialog = emuenv.common_dialog; + auto &ime = emuenv.ime; + + std::lock_guard dialog_lock(dialog.mutex); + std::lock_guard ime_lock(ime.mutex); + + const size_t copy_len = std::min(static_cast(ime.str.length()), + static_cast(dialog.ime.max_length)); + if (dialog.ime.result) { + std::memcpy(dialog.ime.result, ime.str.c_str(), copy_len * sizeof(uint16_t)); + dialog.ime.result[copy_len] = 0; + } + + const std::string utf8 = string_utils::utf16_to_utf8(ime.str); + std::snprintf(dialog.ime.text, sizeof(dialog.ime.text), "%s", utf8.c_str()); + dialog.ime.status = SCE_IME_DIALOG_BUTTON_ENTER; + dialog.status = SCE_COMMON_DIALOG_STATUS_FINISHED; + dialog.result = SCE_COMMON_DIALOG_RESULT_OK; +} + +void cancel_ime_dialog(EmuEnvState &emuenv) { + auto &dialog = emuenv.common_dialog; + if (!dialog.ime.cancelable) + return; + + std::lock_guard dialog_lock(dialog.mutex); + dialog.ime.status = SCE_IME_DIALOG_BUTTON_CLOSE; + dialog.status = SCE_COMMON_DIALOG_STATUS_FINISHED; + dialog.result = SCE_COMMON_DIALOG_RESULT_USER_CANCELED; +} + +bool submit_current_ime(EmuEnvState &emuenv) { + if (!is_any_ime_active(emuenv)) + return false; + + if (is_ime_dialog_active(emuenv)) { + finish_ime_dialog(emuenv); + } else { + std::lock_guard lock(emuenv.ime.mutex); + emuenv.ime.event_id = SCE_IME_EVENT_PRESS_ENTER; + } + + return true; +} + +bool dismiss_current_ime(EmuEnvState &emuenv) { + if (!is_any_ime_active(emuenv)) + return false; + + if (is_ime_dialog_active(emuenv)) { + cancel_ime_dialog(emuenv); + if (emuenv.common_dialog.status != SCE_COMMON_DIALOG_STATUS_FINISHED) + return false; + } else { + std::lock_guard lock(emuenv.ime.mutex); + emuenv.ime.event_id = SCE_IME_EVENT_PRESS_CLOSE; + } + + return true; +} + +ScopedJniCallback::ScopedJniCallback(JNIEnv *env, jobject callback_obj, const char *method_name, const char *method_sig) + : env_(env) { + env->GetJavaVM(&java_vm_); + callback_ = env->NewGlobalRef(callback_obj); + jclass callback_class = env->GetObjectClass(callback_obj); + method_ = env->GetMethodID(callback_class, method_name, method_sig); + env->DeleteLocalRef(callback_class); +} + +ScopedJniCallback::~ScopedJniCallback() { + if (!callback_ || !java_vm_) + return; + + JNIEnv *env = nullptr; + bool attached = false; + const jint result = java_vm_->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + if (result == JNI_EDETACHED) { + attached = java_vm_->AttachCurrentThread(&env, nullptr) == JNI_OK; + if (!env) + return; + } else if (result != JNI_OK) { + return; + } + + env->DeleteGlobalRef(callback_); + callback_ = nullptr; + + if (attached) + java_vm_->DetachCurrentThread(); +} + +void ScopedJniCallback::call_progress(int percent, const char *status) const { + if (!callback_ || !method_ || !env_) + return; + + jstring jstatus = env_->NewStringUTF(status); + env_->CallVoidMethod(callback_, method_, static_cast(percent), jstatus); + env_->DeleteLocalRef(jstatus); + + if (env_->ExceptionCheck()) { + env_->ExceptionDescribe(); + env_->ExceptionClear(); + } +} + +std::optional find_app_by_title_id(const std::string &title_id) { + auto *emuenv = get_emuenv(); + if (!emuenv) + return std::nullopt; + + const auto apps = app::get_apps(*emuenv); + const auto it = std::find_if(apps.begin(), apps.end(), [&](const app::AppEntry &app) { + return app.title_id == title_id; + }); + if (it == apps.end()) + return std::nullopt; + + return *it; +} + +bool remove_path_if_exists(const fs::path &path) { + if (!fs::exists(path)) + return false; + + try { + fs::remove_all(path); + return true; + } catch (const std::exception &error) { + LOG_WARN("Failed to remove '{}': {}", path.string(), error.what()); + return false; + } +} + +uint32_t get_app_action_availability_mask(const std::string &title_id) { + auto *emuenv = get_emuenv(); + auto *controller = get_app_session_controller(); + if (!emuenv) + return 0; + + uint32_t mask = 0; + const auto app = find_app_by_title_id(title_id); + const bool has_app = app.has_value(); + + if (has_app && (!controller || !controller->has_active_session())) + mask |= ACTION_DELETE_APPLICATION; + + if (has_app && !app->savedata.empty() && fs::exists(emuenv->pref_path / "ux0/user" / emuenv->io.user_id / "savedata" / app->savedata)) + mask |= ACTION_DELETE_SAVE_DATA; + + if (fs::exists(emuenv->pref_path / "ux0/patch" / title_id) + || fs::exists(emuenv->shared_path / "patch" / title_id)) + mask |= ACTION_DELETE_PATCH; + + if (has_app && !app->addcont.empty() && fs::exists(emuenv->pref_path / "ux0/addcont" / app->addcont)) + mask |= ACTION_DELETE_DLC; + + if (fs::exists(emuenv->pref_path / "ux0/license" / title_id)) + mask |= ACTION_DELETE_LICENSE; + + if (fs::exists(emuenv->cache_path / "shaders" / title_id)) + mask |= ACTION_DELETE_SHADER_CACHE; + + if (fs::exists(emuenv->cache_path / "shaderlog" / title_id) + || fs::exists(emuenv->log_path / "shaderlog" / title_id)) + mask |= ACTION_DELETE_SHADER_LOG; + + if (fs::exists(emuenv->shared_path / "textures/export" / title_id)) + mask |= ACTION_DELETE_EXPORT_TEXTURES; + + if (fs::exists(emuenv->shared_path / "textures/import" / title_id)) + mask |= ACTION_DELETE_IMPORT_TEXTURES; + + if (has_app) + mask |= ACTION_RESET_LAST_PLAYED; + + return mask; +} diff --git a/vita3k/android/jni/android_state.h b/vita3k/android/jni/android_state.h new file mode 100644 index 000000000..3c20c7c80 --- /dev/null +++ b/vita3k/android/jni/android_state.h @@ -0,0 +1,84 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +struct AndroidSessionState { + std::unique_ptr emuenv; + std::unique_ptr app_session_controller; + Root root_paths; +}; + +AndroidSessionState &android_session_state(); +EmuEnvState *get_emuenv(); +app::AppSessionController *get_app_session_controller(); +Root &get_root_paths(); + +std::string jstring_to_string(JNIEnv *env, jstring str); +bool is_ime_dialog_active(const EmuEnvState &emuenv); +bool is_any_ime_active(const EmuEnvState &emuenv); +void finish_ime_dialog(EmuEnvState &emuenv); +void cancel_ime_dialog(EmuEnvState &emuenv); +bool submit_current_ime(EmuEnvState &emuenv); +bool dismiss_current_ime(EmuEnvState &emuenv); + +class ScopedJniCallback { +public: + ScopedJniCallback(JNIEnv *env, jobject callback_obj, const char *method_name, const char *method_sig); + ~ScopedJniCallback(); + + void call_progress(int percent, const char *status) const; + +private: + JNIEnv *env_ = nullptr; + JavaVM *java_vm_ = nullptr; + jobject callback_ = nullptr; + jmethodID method_ = nullptr; +}; + +std::optional find_app_by_title_id(const std::string &title_id); +bool remove_path_if_exists(const fs::path &path); +void attach_overlay_virtual_controller(); +void detach_overlay_virtual_controller(); + +enum AppActionMask : uint32_t { + ACTION_DELETE_APPLICATION = 1u << 0, + ACTION_DELETE_SAVE_DATA = 1u << 1, + ACTION_DELETE_PATCH = 1u << 2, + ACTION_DELETE_DLC = 1u << 3, + ACTION_DELETE_LICENSE = 1u << 4, + ACTION_DELETE_SHADER_CACHE = 1u << 5, + ACTION_DELETE_SHADER_LOG = 1u << 6, + ACTION_DELETE_EXPORT_TEXTURES = 1u << 7, + ACTION_DELETE_IMPORT_TEXTURES = 1u << 8, + ACTION_RESET_LAST_PLAYED = 1u << 9, +}; + +uint32_t get_app_action_availability_mask(const std::string &title_id); diff --git a/vita3k/host/dialog/src/filesystem_android.cpp b/vita3k/android/jni/filesystem_android.cpp similarity index 51% rename from vita3k/host/dialog/src/filesystem_android.cpp rename to vita3k/android/jni/filesystem_android.cpp index fadc1b73c..732845d22 100644 --- a/vita3k/host/dialog/src/filesystem_android.cpp +++ b/vita3k/android/jni/filesystem_android.cpp @@ -1,5 +1,5 @@ // Vita3K emulator project -// Copyright (C) 2026 Vita3K team +// Copyright (C) 2025 Vita3K team // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,28 +15,36 @@ // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -/** - * @file filesystem.cpp - * @brief Filesystem-related dialogs - * - * This file contains all the source code related to the implementation and - * abstraction of user interface dialogs from the host operating system - * related to filesystem interaction such as file or folder opening dialogs. - * - * The implementation is using the android native file dialog. - */ - -#include - -#include #include #include +#include #include +#include +#include + +#include + +namespace host::dialog::filesystem { + +enum Result { + ERROR, + SUCCESS, + CANCEL, +}; + +struct FileFilter { + std::string display_name = ""; + std::vector file_extensions = {}; +}; + +Result open_file(fs::path &resulting_path, const std::vector &file_filters = {}, const fs::path &default_path = ""); +Result pick_folder(fs::path &resulting_path, const fs::path &default_path = ""); +std::string get_error(); + +} // namespace host::dialog::filesystem static std::atomic file_dialog_running = false; - -// the result from the dialog, this is an UTF-8 string static fs::path dialog_result_path{}; extern "C" JNIEXPORT void JNICALL @@ -48,65 +56,17 @@ Java_org_vita3k_emulator_Emulator_filedialogReturn(JNIEnv *env, jobject thiz, js file_dialog_running.store(false, std::memory_order_release); } -/** - * @brief Format the file extension list of a certain file filter to match the - * format expected by the underlying file browser dialog implementation - * - * @param file_extensions_list File extensions list - * @return std::string A string containing the properly formatted file extension list - */ -std::string format_file_filter_extension_list(const std::vector &file_extensions_list) { - // Formatted string containing the properly formatted file extension list - // - // In the case of nativefiledialog, the expected file extension is a single - // string containing comma-separated list of file extensions - // - // Example: "cpp,cc,txt,..." - std::string formatted_string = ""; - - // For every file extension in the filter list, append it to the formatted string - for (size_t index = 0; index < file_extensions_list.size(); index++) { - // Don't add comma before the first file extension - if (index == 0) { - formatted_string += file_extensions_list.at(index); - } else { - formatted_string += "," + file_extensions_list.at(index); - } - } - - return formatted_string; -}; - static void call_dialog_java_function(const char *name, bool need_write) { - // These permissions are not needed on Android 11+ - if (SDL_GetAndroidSDKVersion() < 30) { - auto callback = [](void *userdata, const char *permission, bool granted) { - SDL_Log("Permission %s was %s", permission, granted ? "granted" : "denied"); - }; + (void)need_write; - SDL_RequestAndroidPermission("android.permission.READ_EXTERNAL_STORAGE", callback, nullptr); - - if (need_write) - SDL_RequestAndroidPermission("android.permission.WRITE_EXTERNAL_STORAGE", callback, nullptr); - } - - // retrieve the JNI environment. JNIEnv *env = reinterpret_cast(SDL_GetAndroidJNIEnv()); - - // retrieve the Java instance of the SDLActivity jobject activity = reinterpret_cast(SDL_GetAndroidActivity()); - - // find the Java class of the activity. It should be SDLActivity or a subclass of it. jclass clazz(env->GetObjectClass(activity)); - - // find the identifier of the method to call jmethodID method_id = env->GetMethodID(clazz, name, "()V"); file_dialog_running = true; - // effectively call the Java method env->CallVoidMethod(activity, method_id); - // clean up the local references. env->DeleteLocalRef(activity); env->DeleteLocalRef(clazz); @@ -115,6 +75,7 @@ static void call_dialog_java_function(const char *name, bool need_write) { } namespace host::dialog::filesystem { + Result open_file(fs::path &resulting_path, const std::vector &file_filters, const fs::path &default_path) { call_dialog_java_function("showFileDialog", false); @@ -122,9 +83,8 @@ Result open_file(fs::path &resulting_path, const std::vector &file_f return Result::CANCEL; resulting_path = std::move(dialog_result_path); - return Result::SUCCESS; -}; +} Result pick_folder(fs::path &resulting_path, const fs::path &default_path) { call_dialog_java_function("showFolderDialog", true); @@ -133,12 +93,11 @@ Result pick_folder(fs::path &resulting_path, const fs::path &default_path) { return Result::CANCEL; resulting_path = std::move(dialog_result_path); - return Result::SUCCESS; -}; +} std::string get_error() { return ""; -}; +} } // namespace host::dialog::filesystem diff --git a/vita3k/android/jni/ime.cpp b/vita3k/android/jni/ime.cpp new file mode 100644 index 000000000..4f1c0b038 --- /dev/null +++ b/vita3k/android/jni/ime.cpp @@ -0,0 +1,142 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "android_state.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace ime { + +static SDL_Window *s_window = nullptr; + +namespace { + +void clear_ime_state(JNIEnv *env, jobject activity, jclass clazz) { + const jmethodID method_id = env->GetMethodID(clazz, "clearNativeImeState", "()V"); + if (method_id) + env->CallVoidMethod(activity, method_id); +} + +void push_ime_state(JNIEnv *env, jobject activity, jclass clazz, EmuEnvState &emuenv) { + const bool dialog_active = is_ime_dialog_active(emuenv); + const bool sce_ime_active = emuenv.ime.state; + + if (!sce_ime_active && !dialog_active) { + clear_ime_state(env, activity, clazz); + return; + } + + std::u16string text; + uint32_t preedit_start = 0; + uint32_t preedit_length = 0; + uint32_t caret_index = 0; + bool multiline = false; + std::string enter_label; + { + std::lock_guard dialog_lock(emuenv.common_dialog.mutex); + std::lock_guard ime_lock(emuenv.ime.mutex); + text = emuenv.ime.str; + preedit_start = emuenv.ime.edit_text.preeditIndex; + preedit_length = emuenv.ime.edit_text.preeditLength; + caret_index = emuenv.ime.edit_text.caretIndex; + multiline = dialog_active + ? emuenv.common_dialog.ime.multiline + : ((emuenv.ime.param.option & SCE_IME_OPTION_MULTILINE) != 0); + enter_label = emuenv.ime.enter_label; + } + + const jmethodID method_id = env->GetMethodID( + clazz, + "updateNativeImeState", + "(ZZLjava/lang/String;IIIZLjava/lang/String;)V"); + if (!method_id) + return; + + const std::string utf8_text = string_utils::utf16_to_utf8(text); + jstring text_value = env->NewStringUTF(utf8_text.c_str()); + jstring enter_label_value = env->NewStringUTF(enter_label.c_str()); + env->CallVoidMethod(activity, + method_id, + static_cast(sce_ime_active), + static_cast(dialog_active), + text_value, + static_cast(preedit_start), + static_cast(preedit_length), + static_cast(caret_index), + static_cast(multiline), + enter_label_value); + env->DeleteLocalRef(text_value); + env->DeleteLocalRef(enter_label_value); +} + +} // namespace + +void set_sdl_window(SDL_Window *window) { + s_window = window; +} + +void set_keyboard_active(bool active) { + if (s_window) { + if (active) + SDL_StartTextInput(s_window); + else + SDL_StopTextInput(s_window); + } + + JNIEnv *env = reinterpret_cast(SDL_GetAndroidJNIEnv()); + jobject activity = reinterpret_cast(SDL_GetAndroidActivity()); + jclass clazz = env->GetObjectClass(activity); + jmethodID method_id = env->GetMethodID(clazz, "setKeyboardActive", "(Z)V"); + if (method_id) + env->CallVoidMethod(activity, method_id, static_cast(active)); + + auto *emuenv = get_emuenv(); + if (emuenv) + push_ime_state(env, activity, clazz, *emuenv); + else + clear_ime_state(env, activity, clazz); + + env->DeleteLocalRef(clazz); + env->DeleteLocalRef(activity); +} + +void notify_ime_state_changed() { + JNIEnv *env = reinterpret_cast(SDL_GetAndroidJNIEnv()); + jobject activity = reinterpret_cast(SDL_GetAndroidActivity()); + if (!env || !activity) + return; + + jclass clazz = env->GetObjectClass(activity); + auto *emuenv = get_emuenv(); + if (clazz) { + if (emuenv) + push_ime_state(env, activity, clazz, *emuenv); + else + clear_ime_state(env, activity, clazz); + env->DeleteLocalRef(clazz); + } + env->DeleteLocalRef(activity); +} + +} // namespace ime diff --git a/vita3k/android/jni/input_overlay.cpp b/vita3k/android/jni/input_overlay.cpp new file mode 100644 index 000000000..c76dc783b --- /dev/null +++ b/vita3k/android/jni/input_overlay.cpp @@ -0,0 +1,120 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "android_state.h" + +#include +#include +#include +#include +#include +#include +#include + +static int virtual_joystick_id = -1; +static SDL_Joystick *virtual_joystick = nullptr; + +static void refresh_overlay_controllers() { + auto *emuenv = get_emuenv(); + auto *controller = get_app_session_controller(); + if (!emuenv || !controller || !controller->has_active_session() || !SDL_WasInit(SDL_INIT_GAMEPAD)) + return; + + refresh_controllers(emuenv->ctrl, *emuenv); +} + +void detach_overlay_virtual_controller() { + if (virtual_joystick) + SDL_CloseJoystick(virtual_joystick); + if (virtual_joystick_id >= 0) + SDL_DetachVirtualJoystick(virtual_joystick_id); + virtual_joystick = nullptr; + virtual_joystick_id = -1; + refresh_overlay_controllers(); +} + +void attach_overlay_virtual_controller() { + detach_overlay_virtual_controller(); + + if (!SDL_WasInit(SDL_INIT_GAMEPAD)) + return; + + SDL_VirtualJoystickDesc desc; + SDL_INIT_INTERFACE(&desc); + desc.type = SDL_JOYSTICK_TYPE_GAMEPAD; + desc.naxes = SDL_GAMEPAD_AXIS_COUNT; + desc.nbuttons = SDL_GAMEPAD_BUTTON_COUNT; + desc.name = "Vita3K Virtual Controller"; + + virtual_joystick_id = SDL_AttachVirtualJoystick(&desc); + if (virtual_joystick_id == 0) { + SDL_Log("Could not create overlay virtual controller: %s", SDL_GetError()); + return; + } + + virtual_joystick = SDL_OpenJoystick(virtual_joystick_id); + if (!virtual_joystick) { + SDL_Log("Could not create virtual joystick: %s", SDL_GetError()); + detach_overlay_virtual_controller(); + return; + } + + refresh_overlay_controllers(); +} + +extern "C" { + +JNIEXPORT void JNICALL +Java_org_vita3k_emulator_overlay_InputOverlay_attachController(JNIEnv *env, jobject thiz) { + attach_overlay_virtual_controller(); +} + +JNIEXPORT void JNICALL +Java_org_vita3k_emulator_overlay_InputOverlay_detachController(JNIEnv *env, jobject thiz) { + detach_overlay_virtual_controller(); +} + +JNIEXPORT void JNICALL +Java_org_vita3k_emulator_overlay_InputOverlay_setAxis(JNIEnv *env, jobject thiz, jint axis, jshort value) { + if (!virtual_joystick) + return; + + SDL_SetJoystickVirtualAxis(virtual_joystick, axis, value); +} + +JNIEXPORT void JNICALL +Java_org_vita3k_emulator_overlay_InputOverlay_setButton(JNIEnv *env, jobject thiz, jint button, jboolean value) { + if (!virtual_joystick) + return; + + if (button < 0) + // l2/r2 + SDL_SetJoystickVirtualAxis(virtual_joystick, -button, value ? SDL_MAX_SINT16 : 0); + else + SDL_SetJoystickVirtualButton(virtual_joystick, button, value); +} + +JNIEXPORT void JNICALL +Java_org_vita3k_emulator_overlay_InputOverlay_setTouchState(JNIEnv *env, jobject thiz, jboolean is_back) { + auto *emuenv = get_emuenv(); + if (!emuenv) + return; + + set_rear_touchscreen(emuenv->touch, static_cast(is_back)); +} + +} // extern "C" diff --git a/vita3k/android/jni/main_android.cpp b/vita3k/android/jni/main_android.cpp new file mode 100644 index 000000000..39af4c337 --- /dev/null +++ b/vita3k/android/jni/main_android.cpp @@ -0,0 +1,521 @@ +// Vita3K emulator project +// Copyright (C) 2026 Vita3K team +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "android_state.h" +#include "interface.h" + +#include +#include