Compare commits
202 commits
Author | SHA1 | Date | |
---|---|---|---|
|
e24f8da113 | ||
|
680cbb559d | ||
|
9dc9bf3baf | ||
|
5f81ac40c8 | ||
|
0deb0f50b8 | ||
|
2b51691d57 | ||
|
11687fe32b | ||
|
9d4d6c5fdc | ||
|
f26b9b174a | ||
|
ce8798fffe | ||
|
3c3dd2bd86 | ||
|
eec1466b7b | ||
|
5910edb9fa | ||
|
63f52580ca | ||
|
d1b80788f6 | ||
|
7eff38ed10 | ||
|
995538fa3e | ||
|
57105076a0 | ||
|
868e946dee | ||
|
fe7fe3ed24 | ||
|
2acbcd333c | ||
|
7853bacddd | ||
|
d827b63044 | ||
|
14e8ee7fe3 | ||
|
bc1edd1f1a | ||
|
cfe767e301 | ||
|
063a80695c | ||
|
a974fc4ac8 | ||
|
ad97506867 | ||
|
cf87efa3c0 | ||
|
88b3dff278 | ||
|
9ecd26d2ce | ||
|
4cfb5c9d55 | ||
|
a6c72531b4 | ||
|
ae43740690 | ||
|
ec964c8610 | ||
|
fd2ce82b6e | ||
|
fd2551439e | ||
|
f3c63abd30 | ||
|
f771952e62 | ||
|
6df92285e1 | ||
|
2c8a3b17b4 | ||
|
0ca46dc05f | ||
|
95eb701e12 | ||
|
2aa29a62cc | ||
|
f20e5be513 | ||
|
5e2161d90b | ||
|
bf587885cb | ||
|
ee63706887 | ||
|
e83b81ec98 | ||
|
8b939a9dab | ||
|
d878bfec3b | ||
|
33889fec1d | ||
|
51dc3c6858 | ||
|
4ea9d76093 | ||
|
d94654ea10 | ||
|
650cf43cd8 | ||
|
54732c560c | ||
|
580d46e3d6 | ||
|
c204ebf021 | ||
|
ea8e403f83 | ||
|
73760e087a | ||
|
3ce9f277d9 | ||
|
378d830a93 | ||
|
16980b0ffc | ||
|
5297c95e52 | ||
|
519f67a20a | ||
|
74f3a2457e | ||
|
18447ac6ac | ||
|
e946e38cd5 | ||
|
beacac3f11 | ||
|
89974af0ef | ||
|
32d5036fc6 | ||
|
86b3c732e5 | ||
|
2ce26ee9ea | ||
|
8559aba978 | ||
|
ff72997511 | ||
|
9a7364ee92 | ||
|
4e35a0b575 | ||
|
6ddb63582f | ||
|
31908f732f | ||
|
4a3d4de9c2 | ||
|
b1850e6f51 | ||
|
1d562cdd11 | ||
|
4a34bcfcbf | ||
|
5fc48cfc8d | ||
|
3947f896e5 | ||
|
1e8cc02d56 | ||
|
b4699d9d47 | ||
|
a53ce91852 | ||
|
01d7ff7a08 | ||
|
493f59cef5 | ||
|
939a6e0f7a | ||
|
975ad17442 | ||
|
dcbf79df14 | ||
|
eba3c2c08f | ||
|
391f91f735 | ||
|
bac344d059 | ||
|
0051182338 | ||
|
527610d599 | ||
|
1ff5042685 | ||
|
b1480396fa | ||
|
de1b520498 | ||
|
a6782e8a13 | ||
|
fd3e4068b6 | ||
|
8fbfb94bec | ||
|
d43597b520 | ||
|
967263fc80 | ||
|
fdbc74b506 | ||
|
544c6b4bbc | ||
|
b8267c0a39 | ||
|
2670b517e8 | ||
|
e90930b0b9 | ||
|
72eb16f933 | ||
|
81ce14e0db | ||
|
d974a0062f | ||
|
7c9ea780b3 | ||
|
4bc1655b05 | ||
|
ebb462f1d5 | ||
|
3a0878f40f | ||
|
240e968d73 | ||
|
52ccaabca8 | ||
|
8acc5e22a0 | ||
|
8dcef46a11 | ||
|
14413d896f | ||
|
bf4eef8e58 | ||
|
2a7a5078fc | ||
|
76a23e4c72 | ||
|
8e477e35bb | ||
|
89d2e67459 | ||
|
32e378a29b | ||
|
12bc825b8a | ||
|
c69b642f54 | ||
|
0e0137a9ff | ||
|
4ea8c6fda5 | ||
|
eb310a4a60 | ||
|
903a9ad81f | ||
|
e341dcf238 | ||
|
8d769ed9cb | ||
|
71c9c0c924 | ||
|
6865b4c8a7 | ||
|
c0bb7abdbc | ||
|
aa58dd5f68 | ||
|
9500859043 | ||
|
93eeb501c0 | ||
|
f298d7551c | ||
|
5ade69f5f4 | ||
|
e6199780a5 | ||
|
2c1fc0199b | ||
|
dec12bd54a | ||
|
5c7622100b | ||
|
eda2d6f9fa | ||
|
dee576bfeb | ||
|
7dda835679 | ||
|
1de19fcbc2 | ||
|
d8077fdea6 | ||
|
9203b23868 | ||
|
61feb3aee2 | ||
|
bd1f5b978b | ||
|
d406c5d81e | ||
|
cfa59dc0bb | ||
|
8cdafaa828 | ||
|
f083a6e5d3 | ||
|
75918be261 | ||
|
2b0d412070 | ||
|
5b910d6f0e | ||
|
4d04f633fa | ||
|
c7fe6333b5 | ||
|
16dac366cf | ||
|
860aace2f5 | ||
|
844b166fbf | ||
|
7e9b5743fb | ||
|
7920188417 | ||
|
3718bab5cb | ||
|
ec9c3dd276 | ||
|
ffb8bf15b2 | ||
|
22934aa46e | ||
|
17a6bfb7dd | ||
|
edb01754ea | ||
|
1259401889 | ||
|
84e2f31415 | ||
|
70be7d987e | ||
|
9763488577 | ||
|
b225e856df | ||
|
2f28911395 | ||
|
d82be7ac7c | ||
|
3783ac9f49 | ||
|
5acb2eee91 | ||
|
19551b1eb6 | ||
|
d59ea25cbe | ||
|
dcd6fe8258 | ||
|
fac8ae2682 | ||
|
9d03856026 | ||
|
0eb4a71720 | ||
|
007b809ad7 | ||
|
66d8e58dcd | ||
|
8cb3dde72f | ||
|
eb3aa52391 | ||
|
c13d2d7208 | ||
|
dac463d74a | ||
|
d72948ca22 | ||
|
ccb26303ad |
10
.ci/linux.sh
|
@ -2,7 +2,10 @@
|
|||
|
||||
if [ "$TARGET" = "appimage" ]; then
|
||||
# Compile the AppImage we distribute with Clang.
|
||||
export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_LINKER=/etc/bin/ld.lld)
|
||||
export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++
|
||||
-DCMAKE_C_COMPILER=clang
|
||||
-DCMAKE_LINKER=/etc/bin/ld.lld
|
||||
-DENABLE_ROOM_STANDALONE=OFF)
|
||||
# Bundle required QT wayland libraries
|
||||
export EXTRA_QT_PLUGINS="waylandcompositor"
|
||||
export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so"
|
||||
|
@ -12,7 +15,7 @@ else
|
|||
fi
|
||||
|
||||
if [ "$GITHUB_REF_TYPE" == "tag" ]; then
|
||||
export EXTRA_CMAKE_FLAGS=($EXTRA_CMAKE_FLAGS -DENABLE_QT_UPDATE_CHECKER=ON)
|
||||
export EXTRA_CMAKE_FLAGS=("${EXTRA_CMAKE_FLAGS[@]}" -DENABLE_QT_UPDATE_CHECKER=ON)
|
||||
fi
|
||||
|
||||
mkdir build && cd build
|
||||
|
@ -21,8 +24,9 @@ cmake .. -G Ninja \
|
|||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DENABLE_QT_TRANSLATION=ON \
|
||||
-DENABLE_ROOM_STANDALONE=OFF \
|
||||
-DUSE_DISCORD_PRESENCE=ON \
|
||||
"${EXTRA_CMAKE_FLAGS[@]}"
|
||||
"${EXTRA_CMAKE_FLAGS[@]}"
|
||||
ninja
|
||||
strip -s bin/Release/*
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ BASE_ARTIFACT_ARCH="${BASE_ARTIFACT##*-}"
|
|||
mv $BASE_ARTIFACT $BUNDLE_DIR
|
||||
|
||||
# Executable binary paths that need to be combined.
|
||||
BIN_PATHS=(azahar-room Azahar.app/Contents/MacOS/azahar)
|
||||
BIN_PATHS=(Azahar.app/Contents/MacOS/azahar)
|
||||
|
||||
# Dylib paths that need to be combined.
|
||||
IFS=$'\n'
|
||||
|
@ -36,8 +36,11 @@ for OTHER_ARTIFACT in "${ARTIFACTS_LIST[@]:1}"; do
|
|||
done
|
||||
done
|
||||
|
||||
# Remove leftover libs so that they aren't distributed
|
||||
rm -rf "${BUNDLE_DIR}/libs"
|
||||
|
||||
# Re-sign executables and bundles after combining.
|
||||
APP_PATHS=(azahar-room Azahar.app)
|
||||
APP_PATHS=(Azahar.app)
|
||||
for APP_PATH in "${APP_PATHS[@]}"; do
|
||||
codesign --deep -fs - $BUNDLE_DIR/$APP_PATH
|
||||
done
|
||||
|
|
|
@ -11,6 +11,7 @@ cmake .. -GNinja \
|
|||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DENABLE_QT_TRANSLATION=ON \
|
||||
-DENABLE_ROOM_STANDALONE=OFF \
|
||||
-DUSE_DISCORD_PRESENCE=ON \
|
||||
"${EXTRA_CMAKE_FLAGS[@]}"
|
||||
ninja
|
||||
|
|
|
@ -14,7 +14,7 @@ else
|
|||
fi
|
||||
|
||||
# Archive and upload the artifacts.
|
||||
mkdir artifacts
|
||||
mkdir -p artifacts
|
||||
|
||||
function pack_artifacts() {
|
||||
ARTIFACTS_PATH="$1"
|
||||
|
@ -50,11 +50,6 @@ function pack_artifacts() {
|
|||
rm -rf "$REV_NAME"
|
||||
}
|
||||
|
||||
if [ "$OS" = "windows" ] && [ "$GITHUB_REF_TYPE" = "tag" ]; then
|
||||
# Move the installer to the artifacts directory
|
||||
mv src/installer/bin/*.exe artifacts/
|
||||
fi
|
||||
|
||||
if [ -n "$UNPACKED" ]; then
|
||||
# Copy the artifacts to be uploaded unpacked.
|
||||
for ARTIFACT in build/bundle/*; do
|
||||
|
|
|
@ -4,6 +4,10 @@ GITDATE="`git show -s --date=short --format='%ad' | sed 's/-//g'`"
|
|||
GITREV="`git show -s --format='%h'`"
|
||||
REV_NAME="azahar-unified-source-${GITDATE}-${GITREV}"
|
||||
|
||||
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
|
||||
REV_NAME="azahar-unified-source-$GITHUB_REF_NAME"
|
||||
fi
|
||||
|
||||
COMPAT_LIST='dist/compatibility_list/compatibility_list.json'
|
||||
|
||||
mkdir artifacts
|
||||
|
|
15
.github/workflows/build.yml
vendored
|
@ -49,9 +49,11 @@ jobs:
|
|||
${{ runner.os }}-${{ matrix.target }}-
|
||||
- name: Build
|
||||
run: ./.ci/linux.sh
|
||||
- name: Pack
|
||||
- name: Move AppImage to artifacts directory
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
run: ./.ci/pack.sh
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
mv build/bundle/*.AppImage artifacts/
|
||||
- name: Upload
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
@ -156,7 +158,7 @@ jobs:
|
|||
- name: Install vulkan-sdk (MSVC)
|
||||
if: ${{ matrix.target == 'msvc' }}
|
||||
run: |
|
||||
wget https://sdk.lunarg.com/sdk/download/1.3.296.0/windows/VulkanSDK-1.3.296.0-Installer.exe -O D:/a/_temp/vulkan.exe
|
||||
wget https://sdk.lunarg.com/sdk/download/1.4.304.1/windows/VulkanSDK-1.4.304.1-Installer.exe -O D:/a/_temp/vulkan.exe
|
||||
D:/a/_temp/vulkan.exe --accept-licenses --default-answer --confirm-command install
|
||||
- name: Set up MSYS2
|
||||
if: ${{ matrix.target == 'msys2' }}
|
||||
|
@ -188,8 +190,8 @@ jobs:
|
|||
run: |
|
||||
cd src\installer
|
||||
"C:\Program Files (x86)\NSIS\makensis.exe" /DPRODUCT_VARIANT=${{ matrix.target }} /DPRODUCT_VERSION=${{ github.ref_name }} citra.nsi
|
||||
mkdir bin
|
||||
move /y *.exe bin\
|
||||
mkdir ..\..\artifacts 2> NUL
|
||||
move /y *.exe ..\..\artifacts\
|
||||
shell: cmd
|
||||
- name: Pack
|
||||
run: ./.ci/pack.sh
|
||||
|
@ -230,6 +232,9 @@ jobs:
|
|||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install ccache apksigner -y
|
||||
- name: Update Android SDK CMake version
|
||||
run: |
|
||||
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
|
||||
- name: Build
|
||||
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
||||
env:
|
||||
|
|
23
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: azahar-stale
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
stale-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: 10
|
||||
stale-issue-message: "This issue has been marked as stale. If there is no activity within the next 10 days, this issue will be closed."
|
||||
close-issue-message: "This issue has been closed as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
remove-issue-stale-when-updated: true
|
||||
exempt-issue-labels: "priority - low,priority - medium,priority - high,priority - urgent,documentation,enhancement,miscellaneous,task,refactor"
|
1
.gitignore
vendored
|
@ -13,6 +13,7 @@ src/common/scm_rev.cpp
|
|||
# Project/editor files
|
||||
*.swp
|
||||
*.kdev4
|
||||
.markdown-preview.html
|
||||
.idea/
|
||||
.vs/
|
||||
.vscode/
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# CMake 3.12 required for 20 to be a valid value for CXX_STANDARD
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
# CMake >=3.12 required for 20 to be a valid value for CXX_STANDARD,
|
||||
# and >=3.25 required to make LTO work on Android.
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
|
||||
# Don't override the warning flags in MSVC:
|
||||
cmake_policy(SET CMP0092 NEW)
|
||||
|
@ -41,8 +42,8 @@ if (APPLE)
|
|||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
else()
|
||||
# Minimum macOS 11
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0")
|
||||
# Minimum macOS 13
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.4")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
@ -86,7 +87,8 @@ option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
|
|||
option(ENABLE_QT_UPDATE_CHECKER "Enable built-in update checker for the Qt frontend" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_TESTS "Enable generating tests executable" ON "NOT IOS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_ROOM "Enable generating dedicated room executable" ON "NOT ANDROID AND NOT IOS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_ROOM "Enable dedicated room functionality" ON "NOT ANDROID AND NOT IOS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_ROOM_STANDALONE "Enable generating a standalone dedicated room executable" ON "ENABLE_ROOM" OFF)
|
||||
|
||||
option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
|
||||
option(ENABLE_SCRIPTING "Enable RPC server for scripting" ON)
|
||||
|
@ -104,6 +106,8 @@ option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
|||
|
||||
option(ENABLE_MICROPROFILE "Enables microprofile capabilities" OFF)
|
||||
|
||||
option(ENABLE_SSE42 "Enable SSE4.2 optimizations on x86_64" ON)
|
||||
|
||||
# Compile options
|
||||
CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF)
|
||||
option(ENABLE_LTO "Enable link time optimization" ${DEFAULT_ENABLE_LTO})
|
||||
|
@ -118,10 +122,22 @@ endif()
|
|||
if (ENABLE_QT_TRANSLATION)
|
||||
add_definitions(-DENABLE_QT_TRANSLATION)
|
||||
endif()
|
||||
if (ENABLE_ROOM)
|
||||
add_definitions(-DENABLE_ROOM)
|
||||
endif()
|
||||
if (ENABLE_SDL2_FRONTEND)
|
||||
add_definitions(-DENABLE_SDL2_FRONTEND)
|
||||
endif()
|
||||
|
||||
if(ENABLE_SSE42 AND (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64"))
|
||||
message(STATUS "SSE4.2 enabled for x86_64")
|
||||
if(MSVC)
|
||||
SET(SSE42_COMPILE_OPTION /arch:SSE4.2)
|
||||
else()
|
||||
SET(SSE42_COMPILE_OPTION -msse4.1 -msse4.2)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(CitraHandleSystemLibs)
|
||||
|
||||
if (CITRA_USE_PRECOMPILED_HEADERS)
|
||||
|
@ -484,9 +500,8 @@ if (NOT ANDROID AND NOT IOS)
|
|||
elseif (ENABLE_SDL2_FRONTEND)
|
||||
bundle_target(citra_meta)
|
||||
endif()
|
||||
|
||||
if (ENABLE_ROOM)
|
||||
bundle_target(citra_room)
|
||||
if (ENABLE_ROOM_STANDALONE)
|
||||
bundle_target(citra_room_standalone)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
|
@ -279,22 +279,12 @@ else()
|
|||
add_custom_target(bundle)
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/")
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/"
|
||||
POST_BUILD)
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/dist/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dist/azahar.png" "${CMAKE_BINARY_DIR}/bundle/dist/azahar.png")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/license.txt" "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/README.md" "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting")
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting"
|
||||
POST_BUILD)
|
||||
|
||||
# On Linux, add a command to prepare linuxdeploy and any required plugins before any bundling occurs.
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
|
@ -305,7 +295,8 @@ else()
|
|||
"-DLINUXDEPLOY_PATH=${CMAKE_BINARY_DIR}/externals/linuxdeploy"
|
||||
"-DLINUXDEPLOY_ARCH=${CMAKE_HOST_SYSTEM_PROCESSOR}"
|
||||
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||
POST_BUILD)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
|
|
|
@ -171,16 +171,16 @@ endfunction()
|
|||
|
||||
function(download_moltenvk)
|
||||
if (IOS)
|
||||
set(MOLTENVK_PLATFORM "iOS")
|
||||
set(MOLTENVK_PLATFORM "static/MoltenVK.xcframework/ios-arm64")
|
||||
else()
|
||||
set(MOLTENVK_PLATFORM "macOS")
|
||||
set(MOLTENVK_PLATFORM "dynamic/dylib/macOS")
|
||||
endif()
|
||||
|
||||
set(MOLTENVK_DIR "${CMAKE_BINARY_DIR}/externals/MoltenVK")
|
||||
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
|
||||
if (NOT EXISTS ${MOLTENVK_DIR})
|
||||
if (NOT EXISTS ${MOLTENVK_TAR})
|
||||
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.7-rc2/MoltenVK-all.tar
|
||||
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.9/MoltenVK-all.tar
|
||||
${MOLTENVK_TAR} SHOW_PROGRESS)
|
||||
endif()
|
||||
|
||||
|
@ -189,7 +189,7 @@ function(download_moltenvk)
|
|||
endif()
|
||||
|
||||
# Add the MoltenVK library path to the prefix so find_library can locate it.
|
||||
list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/dylib/${MOLTENVK_PLATFORM}")
|
||||
list(APPEND CMAKE_PREFIX_PATH "${MOLTENVK_DIR}/MoltenVK/${MOLTENVK_PLATFORM}")
|
||||
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
|
|
38
README.md
|
@ -10,23 +10,27 @@ It was created from the merging of PabloMK7's Citra fork and the Lime3DS project
|
|||
|
||||
The goal of this project is to be the de-facto platform for future development.
|
||||
|
||||
> [!NOTE]
|
||||
> Azahar has not fully released yet. For this reason, there are no compiled binaries available for download.
|
||||
>
|
||||
> It is recommended that only developers and early adopters should use the emulator until our first stable release.
|
||||
>
|
||||
> Here be dragons.
|
||||
|
||||
<!--
|
||||
# Installation
|
||||
|
||||
### Windows & MacOS
|
||||
### Windows
|
||||
|
||||
Download the latest release from [Releases](https://github.com/azahar-emu/azahar/releases).
|
||||
|
||||
If you are unsure of whether you want to use MSYS2 or MSVC, use MSYS2.
|
||||
|
||||
---
|
||||
|
||||
### MacOS
|
||||
|
||||
Download the latest release from [Releases](https://github.com/azahar-emu/azahar/releases).
|
||||
|
||||
The `macos-universal` download will work on both Intel and Apple Silicon Macs.
|
||||
|
||||
---
|
||||
### Android
|
||||
The recommended method of downloading Azahar on Android is via the [Google Play store](https://play.google.com/store/apps/details?id=io.github.lime3ds.android).
|
||||
The recommended method of downloading Azahar on Android is via the Google Play store:
|
||||
|
||||
<a href='https://play.google.com/store/apps/details?id=io.github.lime3ds.android'><img width='180' alt='Get it on Google Play' src='https://raw.githubusercontent.com/pioug/google-play-badges/06ccd9252af1501613da2ca28eaffe31307a4e6d/svg/English.svg'/></a>
|
||||
|
||||
Alternatively, you can install the app using Obtainium:
|
||||
1. Download and install Obtainium from [here](https://github.com/ImranR98/Obtainium/releases) (use the file named `app-release.apk`)
|
||||
|
@ -42,12 +46,12 @@ Keep in mind that you will not recieve automatic updates when installing via the
|
|||
---
|
||||
### Linux
|
||||
|
||||
Azahar is available as an AppImage on the [Releases](https://github.com/azahar-emu/azahar/releases) page.
|
||||
The recommended format for using Azahar on Linux is the Flatpak available on Flathub:
|
||||
|
||||
We are also on Flathub:
|
||||
<a href='https://flathub.org/apps/org.azahar_emu.Azahar'><img width='180' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.png'/></a>
|
||||
|
||||
Azahar is also available as an AppImage on the [Releases](https://github.com/azahar-emu/azahar/releases) page.
|
||||
|
||||
<a href=https://flathub.org/apps/org.azahar-emu.azahar><img width='180' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.png'/></a>
|
||||
-->
|
||||
|
||||
# Build instructions
|
||||
|
||||
|
@ -83,14 +87,14 @@ Below are the minimum requirements to run Azahar:
|
|||
|
||||
### Desktop
|
||||
```
|
||||
Operating System: Windows 10 (64-bit), MacOS 13 (Ventura), or modern 64-bit Linux
|
||||
CPU: x86-64 (64-bit) CPU. Single core performance higher than 1,800 on Passmark
|
||||
Operating System: Windows 10 (64-bit), MacOS 13.4 (Ventura), or modern 64-bit Linux
|
||||
CPU: x86-64/ARM64 CPU (Windows for ARM not supported). Single core performance higher than 1,800 on Passmark
|
||||
GPU: OpenGL 4.3 or Vulkan 1.1 support
|
||||
Memory: 2GB of RAM. 4GB is recommended
|
||||
```
|
||||
### Android
|
||||
```
|
||||
Operating System: Android 9.0+
|
||||
Operating System: Android 9.0+ (64-bit)
|
||||
CPU: Snapdragon 835 SoC or better
|
||||
GPU: OpenGL ES 3.2 or Vulkan 1.1 support
|
||||
Memory: 2GB of RAM. 4GB is recommended
|
||||
|
|
1
dist/azahar.desktop
vendored
|
@ -12,4 +12,3 @@ Exec=azahar %f
|
|||
Categories=Game;Emulator;
|
||||
MimeType=application/x-ctr-3dsx;application/x-ctr-cci;application/x-ctr-cia;application/x-ctr-cxi;
|
||||
Keywords=3DS;Nintendo;
|
||||
PrefersNonDefaultGPU=true
|
||||
|
|
2
dist/compatibility_list
vendored
|
@ -1 +1 @@
|
|||
Subproject commit b153099f511759824941f5797ca69e2d39e57de3
|
||||
Subproject commit 31c12299126baf892b965defc6ba7810b9c42ccf
|
1101
dist/languages/ca_ES_valencia.ts
vendored
1117
dist/languages/da_DK.ts
vendored
1151
dist/languages/de.ts
vendored
1048
dist/languages/el.ts
vendored
1085
dist/languages/es_ES.ts
vendored
995
dist/languages/fi.ts
vendored
1129
dist/languages/fr.ts
vendored
1047
dist/languages/hu_HU.ts
vendored
1011
dist/languages/id.ts
vendored
1450
dist/languages/it.ts
vendored
1051
dist/languages/ja_JP.ts
vendored
995
dist/languages/ko_KR.ts
vendored
995
dist/languages/lt_LT.ts
vendored
995
dist/languages/nb.ts
vendored
1113
dist/languages/nl.ts
vendored
1101
dist/languages/pl_PL.ts
vendored
1093
dist/languages/pt_BR.ts
vendored
987
dist/languages/ro_RO.ts
vendored
1678
dist/languages/ru_RU.ts
vendored
1097
dist/languages/sv.ts
vendored
1643
dist/languages/tr_TR.ts
vendored
995
dist/languages/vi_VN.ts
vendored
1089
dist/languages/zh_CN.ts
vendored
995
dist/languages/zh_TW.ts
vendored
61
dist/scripting/citra.py
vendored
|
@ -1,15 +1,21 @@
|
|||
# Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
# Licensed under GPLv2 or any later version
|
||||
# Refer to the license.txt file included.
|
||||
|
||||
import struct
|
||||
import random
|
||||
import enum
|
||||
import socket
|
||||
|
||||
CURRENT_REQUEST_VERSION = 1
|
||||
MAX_REQUEST_DATA_SIZE = 32
|
||||
MAX_PACKET_SIZE = 48
|
||||
MAX_REQUEST_DATA_SIZE = 1024
|
||||
MAX_PACKET_SIZE = 1024 + 0x10
|
||||
|
||||
class RequestType(enum.IntEnum):
|
||||
ReadMemory = 1,
|
||||
WriteMemory = 2
|
||||
WriteMemory = 2,
|
||||
ProcessList = 3,
|
||||
SetGetProcess = 4,
|
||||
|
||||
CITRA_PORT = 45987
|
||||
|
||||
|
@ -34,6 +40,55 @@ class Citra:
|
|||
return raw_reply[4*4:]
|
||||
return None
|
||||
|
||||
def process_list(self):
|
||||
processes = {}
|
||||
read_processes = 0
|
||||
while True:
|
||||
request_data = struct.pack("II", read_processes, 0x7FFFFFFF)
|
||||
request, request_id = self._generate_header(RequestType.ProcessList, len(request_data))
|
||||
request += request_data
|
||||
self.socket.sendto(request, (self.address, CITRA_PORT))
|
||||
|
||||
raw_reply = self.socket.recv(MAX_PACKET_SIZE)
|
||||
reply_data = self._read_and_validate_header(raw_reply, request_id, RequestType.ProcessList)
|
||||
|
||||
if reply_data:
|
||||
read_count = struct.unpack("I", reply_data[0:4])[0]
|
||||
reply_data = reply_data[4:]
|
||||
if read_count == 0:
|
||||
break
|
||||
read_processes += read_count
|
||||
for i in range(read_count):
|
||||
proc_data = reply_data[i * 0x14 : (i + 1) * 0x14]
|
||||
proc_id, title_id, proc_name = struct.unpack("<IQ8s", proc_data)
|
||||
proc_name = proc_name.rstrip(b"\x00").decode("ascii")
|
||||
processes[proc_id] = (title_id, proc_name)
|
||||
else:
|
||||
break
|
||||
return processes
|
||||
|
||||
def get_process(self):
|
||||
request_data = struct.pack("II", 0, 0)
|
||||
request, request_id = self._generate_header(RequestType.SetGetProcess, len(request_data))
|
||||
request += request_data
|
||||
self.socket.sendto(request, (self.address, CITRA_PORT))
|
||||
|
||||
raw_reply = self.socket.recv(MAX_PACKET_SIZE)
|
||||
reply_data = self._read_and_validate_header(raw_reply, request_id, RequestType.SetGetProcess)
|
||||
|
||||
if reply_data:
|
||||
return struct.unpack("I", reply_data)[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_process(self, process_id):
|
||||
request_data = struct.pack("II", 1, process_id)
|
||||
request, request_id = self._generate_header(RequestType.SetGetProcess, len(request_data))
|
||||
request += request_data
|
||||
self.socket.sendto(request, (self.address, CITRA_PORT))
|
||||
|
||||
self.socket.recv(MAX_PACKET_SIZE)
|
||||
|
||||
def read_memory(self, read_address, read_size):
|
||||
"""
|
||||
>>> c.read_memory(0x100000, 4)
|
||||
|
|
2
externals/libyuv
vendored
|
@ -1 +1 @@
|
|||
Subproject commit c060118bea3f28ceb837d3c85e479d3bb4c21726
|
||||
Subproject commit 6f729fbe658a40dfd993fa8b22bd612bb17cde5c
|
|
@ -136,6 +136,9 @@ else()
|
|||
# which is a problem for older versions (e.g. GCC 11).
|
||||
add_compile_options("-Wno-attributes")
|
||||
add_compile_options("-Wno-interference-size")
|
||||
# Suppress irrelevant ABI warnings generated by GCC
|
||||
add_compile_options("-Wno-psabi")
|
||||
add_link_options("-Wno-psabi")
|
||||
endif()
|
||||
|
||||
if (MINGW)
|
||||
|
@ -199,6 +202,10 @@ if (ENABLE_ROOM)
|
|||
add_subdirectory(citra_room)
|
||||
endif()
|
||||
|
||||
if (ENABLE_ROOM_STANDALONE)
|
||||
add_subdirectory(citra_room_standalone)
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
add_subdirectory(android/app/src/main/jni)
|
||||
target_include_directories(citra-android PRIVATE android/app/src/main)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Citra Emulator Project / Lime3DS Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -22,7 +22,7 @@ plugins {
|
|||
val autoVersion = (((System.currentTimeMillis() / 1000) - 1451606400) / 10).toInt()
|
||||
val abiFilter = listOf("arm64-v8a", "x86_64")
|
||||
|
||||
val downloadedJniLibsPath = "${buildDir}/downloadedJniLibs"
|
||||
val downloadedJniLibsPath = "${layout.buildDirectory.get().asFile.path}/downloadedJniLibs"
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
|
@ -51,6 +51,7 @@ android {
|
|||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
lint {
|
||||
|
@ -124,7 +125,6 @@ android {
|
|||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
|
@ -150,7 +150,7 @@ android {
|
|||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version = "3.22.1"
|
||||
version = "3.25.0+"
|
||||
path = file("../../../CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
@ -186,8 +186,8 @@ dependencies {
|
|||
|
||||
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
||||
val downloadVulkanValidationLayers = tasks.register<Download>("downloadVulkanValidationLayers") {
|
||||
src("https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/sdk-1.3.261.1/android-binaries-sdk-1.3.261.1-android.zip")
|
||||
dest(file("${buildDir}/tmp/Vulkan-ValidationLayers.zip"))
|
||||
src("https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.4.304.1/android-binaries-1.4.304.1.zip")
|
||||
dest(file("${layout.buildDirectory.get().asFile.path}/tmp/Vulkan-ValidationLayers.zip"))
|
||||
onlyIfModified(true)
|
||||
}
|
||||
|
||||
|
|
|
@ -186,6 +186,10 @@ object NativeLibrary {
|
|||
|
||||
external fun unlinkConsole()
|
||||
|
||||
external fun setTemporaryFrameLimit(speed: Double)
|
||||
|
||||
external fun disableTemporaryFrameLimit()
|
||||
|
||||
private var coreErrorAlertResult = false
|
||||
private val coreErrorAlertLock = Object()
|
||||
|
||||
|
@ -773,6 +777,7 @@ object NativeLibrary {
|
|||
const val BUTTON_DEBUG = 781
|
||||
const val BUTTON_GPIO14 = 782
|
||||
const val BUTTON_SWAP = 800
|
||||
const val BUTTON_TURBO = 801
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Citra Emulator Project / Lime3DS Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -15,6 +15,7 @@ import android.os.Bundle
|
|||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -44,6 +45,7 @@ import org.citra.citra_emu.utils.FileBrowserHelper
|
|||
import org.citra.citra_emu.utils.EmulationLifecycleUtil
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.utils.TurboHelper
|
||||
import org.citra.citra_emu.viewmodel.EmulationViewModel
|
||||
|
||||
class EmulationActivity : AppCompatActivity() {
|
||||
|
@ -51,7 +53,7 @@ class EmulationActivity : AppCompatActivity() {
|
|||
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
var isActivityRecreated = false
|
||||
private val emulationViewModel: EmulationViewModel by viewModels()
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
val settingsViewModel: SettingsViewModel by viewModels()
|
||||
|
||||
private lateinit var binding: ActivityEmulationBinding
|
||||
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
|
||||
|
@ -67,6 +69,8 @@ class EmulationActivity : AppCompatActivity() {
|
|||
private var isEmulationRunning: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
ThemeUtil.setTheme(this)
|
||||
|
||||
settingsViewModel.settings.loadSettings()
|
||||
|
@ -192,9 +196,16 @@ class EmulationActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun enableFullscreenImmersive() {
|
||||
// TODO: Remove this once we properly account for display insets in the input overlay
|
||||
window.attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
||||
val attributes = window.attributes
|
||||
|
||||
attributes.layoutInDisplayCutoutMode =
|
||||
if (BooleanSetting.EXPAND_TO_CUTOUT_AREA.boolean) {
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
} else {
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
||||
}
|
||||
|
||||
window.attributes = attributes
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
|
@ -227,20 +238,26 @@ class EmulationActivity : AppCompatActivity() {
|
|||
preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
|
||||
val action: Int = when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
hotkeyUtility.handleHotkey(button)
|
||||
|
||||
// On some devices, the back gesture / button press is not intercepted by androidx
|
||||
// and fails to open the emulation menu. So we're stuck running deprecated code to
|
||||
// cover for either a fault on androidx's side or in OEM skins (MIUI at least)
|
||||
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
onBackPressed()
|
||||
// If the hotkey is pressed, we don't want to open the drawer
|
||||
if (!hotkeyUtility.HotkeyIsPressed) {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
hotkeyUtility.handleHotkey(button)
|
||||
|
||||
// Normal key events.
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
}
|
||||
|
||||
KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
|
||||
KeyEvent.ACTION_UP -> {
|
||||
hotkeyUtility.HotkeyIsPressed = false
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
val input = event.device
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright Citra Emulator Project / Lime3DS Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.text.TextUtils
|
||||
|
@ -19,7 +20,12 @@ import android.graphics.drawable.BitmapDrawable
|
|||
import android.graphics.Bitmap
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.findNavController
|
||||
|
@ -28,6 +34,7 @@ import androidx.recyclerview.widget.AsyncDifferConfig
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.widget.PopupMenu
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
@ -41,17 +48,29 @@ import org.citra.citra_emu.CitraApplication
|
|||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
||||
import org.citra.citra_emu.databinding.CardGameBinding
|
||||
import org.citra.citra_emu.databinding.DialogShortcutBinding
|
||||
import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.fragments.IndeterminateProgressDialogFragment
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity, private val inflater: LayoutInflater) :
|
||||
class GameAdapter(private val activity: AppCompatActivity, private val inflater: LayoutInflater, private val openImageLauncher: ActivityResultLauncher<String>?) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
private var lastClickTime = 0L
|
||||
private var imagePath: String? = null
|
||||
private var dialogShortcutBinding: DialogShortcutBinding? = null
|
||||
|
||||
fun handleShortcutImageResult(uri: Uri?) {
|
||||
val path = uri?.toString()
|
||||
if (path != null) {
|
||||
imagePath = path
|
||||
dialogShortcutBinding!!.imageScaleSwitch.isEnabled = imagePath != null
|
||||
refreshShortcutDialogIcon()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||
// Create a new view.
|
||||
|
@ -203,6 +222,117 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||
}
|
||||
}
|
||||
|
||||
private data class GameDirectories(
|
||||
val gameDir: String,
|
||||
val saveDir: String,
|
||||
val modsDir: String,
|
||||
val texturesDir: String,
|
||||
val appDir: String,
|
||||
val dlcDir: String,
|
||||
val updatesDir: String,
|
||||
val extraDir: String
|
||||
)
|
||||
private fun getGameDirectories(game: Game): GameDirectories {
|
||||
val basePath = "sdmc/Nintendo 3DS/00000000000000000000000000000000/00000000000000000000000000000000"
|
||||
return GameDirectories(
|
||||
gameDir = game.path.substringBeforeLast("/"),
|
||||
saveDir = basePath + "/title/${String.format("%016x", game.titleId).lowercase().substring(0, 8)}/${String.format("%016x", game.titleId).lowercase().substring(8)}/data/00000001",
|
||||
modsDir = "load/mods/${String.format("%016X", game.titleId)}",
|
||||
texturesDir = "load/textures/${String.format("%016X", game.titleId)}",
|
||||
appDir = game.path.substringBeforeLast("/").split("/").filter { it.isNotEmpty() }.joinToString("/"),
|
||||
dlcDir = basePath + "/title/0004008c/${String.format("%016x", game.titleId).lowercase().substring(8)}/content",
|
||||
updatesDir = basePath + "/title/0004000e/${String.format("%016x", game.titleId).lowercase().substring(8)}/content",
|
||||
extraDir = basePath + "/extdata/00000000/${String.format("%016X", game.titleId).substring(8, 14).padStart(8, '0')}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun showOpenContextMenu(view: View, game: Game) {
|
||||
val dirs = getGameDirectories(game)
|
||||
|
||||
val popup = PopupMenu(view.context, view).apply {
|
||||
menuInflater.inflate(R.menu.game_context_menu_open, menu)
|
||||
listOf(
|
||||
R.id.game_context_open_app to dirs.appDir,
|
||||
R.id.game_context_open_save_dir to dirs.saveDir,
|
||||
R.id.game_context_open_updates to dirs.updatesDir,
|
||||
R.id.game_context_open_dlc to dirs.dlcDir,
|
||||
R.id.game_context_open_extra to dirs.extraDir
|
||||
).forEach { (id, dir) ->
|
||||
menu.findItem(id)?.isEnabled =
|
||||
CitraApplication.documentsTree.folderUriHelper(dir)?.let {
|
||||
DocumentFile.fromTreeUri(view.context, it)?.exists()
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setType("*/*")
|
||||
|
||||
val uri = when (menuItem.itemId) {
|
||||
R.id.game_context_open_app -> CitraApplication.documentsTree.folderUriHelper(dirs.appDir)
|
||||
R.id.game_context_open_save_dir -> CitraApplication.documentsTree.folderUriHelper(dirs.saveDir)
|
||||
R.id.game_context_open_updates -> CitraApplication.documentsTree.folderUriHelper(dirs.updatesDir)
|
||||
R.id.game_context_open_dlc -> CitraApplication.documentsTree.folderUriHelper(dirs.dlcDir)
|
||||
R.id.game_context_open_extra -> CitraApplication.documentsTree.folderUriHelper(dirs.extraDir)
|
||||
R.id.game_context_open_textures -> CitraApplication.documentsTree.folderUriHelper(dirs.texturesDir, true)
|
||||
R.id.game_context_open_mods -> CitraApplication.documentsTree.folderUriHelper(dirs.modsDir, true)
|
||||
else -> null
|
||||
}
|
||||
|
||||
uri?.let {
|
||||
intent.data = it
|
||||
view.context.startActivity(intent)
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun showUninstallContextMenu(view: View, game: Game, bottomSheetDialog: BottomSheetDialog) {
|
||||
val dirs = getGameDirectories(game)
|
||||
val popup = PopupMenu(view.context, view).apply {
|
||||
menuInflater.inflate(R.menu.game_context_menu_uninstall, menu)
|
||||
listOf(
|
||||
R.id.game_context_uninstall to dirs.gameDir,
|
||||
R.id.game_context_uninstall_dlc to dirs.dlcDir,
|
||||
R.id.game_context_uninstall_updates to dirs.updatesDir
|
||||
).forEach { (id, dir) ->
|
||||
menu.findItem(id)?.isEnabled =
|
||||
CitraApplication.documentsTree.folderUriHelper(dir)?.let {
|
||||
DocumentFile.fromTreeUri(view.context, it)?.exists()
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { menuItem ->
|
||||
val uninstallAction: () -> Unit = {
|
||||
when (menuItem.itemId) {
|
||||
R.id.game_context_uninstall -> CitraApplication.documentsTree.deleteDocument(dirs.gameDir)
|
||||
R.id.game_context_uninstall_dlc -> FileUtil.deleteDocument(CitraApplication.documentsTree.folderUriHelper(dirs.dlcDir)
|
||||
.toString())
|
||||
R.id.game_context_uninstall_updates -> FileUtil.deleteDocument(CitraApplication.documentsTree.folderUriHelper(dirs.updatesDir)
|
||||
.toString())
|
||||
}
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
if (menuItem.itemId in listOf(R.id.game_context_uninstall, R.id.game_context_uninstall_dlc, R.id.game_context_uninstall_updates)) {
|
||||
IndeterminateProgressDialogFragment.newInstance(activity, R.string.uninstalling, false, uninstallAction)
|
||||
.show(activity.supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun showAboutGameDialog(context: Context, game: Game, holder: GameViewHolder, view: View) {
|
||||
val bottomSheetView = inflater.inflate(R.layout.dialog_about_game, null)
|
||||
|
||||
|
@ -222,21 +352,67 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||
}
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.game_shortcut).setOnClickListener {
|
||||
val shortcutManager = activity.getSystemService(ShortcutManager::class.java)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val bitmap = (bottomSheetView.findViewById<ImageView>(R.id.game_icon).drawable as BitmapDrawable).bitmap
|
||||
val icon = Icon.createWithBitmap(bitmap)
|
||||
|
||||
val shortcut = ShortcutInfo.Builder(context, game.title)
|
||||
.setShortLabel(game.title)
|
||||
.setIcon(icon)
|
||||
.setIntent(game.launchIntent.apply {
|
||||
putExtra("launched_from_shortcut", true)
|
||||
})
|
||||
.build()
|
||||
shortcutManager.requestPinShortcut(shortcut, null)
|
||||
// Default to false for zoomed in shortcut icons
|
||||
preferences.edit() {
|
||||
putBoolean(
|
||||
"shouldStretchIcon",
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
dialogShortcutBinding = DialogShortcutBinding.inflate(activity.layoutInflater)
|
||||
|
||||
dialogShortcutBinding!!.shortcutNameInput.setText(game.title)
|
||||
GameIconUtils.loadGameIcon(activity, game, dialogShortcutBinding!!.shortcutIcon)
|
||||
|
||||
dialogShortcutBinding!!.shortcutIcon.setOnClickListener {
|
||||
openImageLauncher?.launch("image/*")
|
||||
}
|
||||
|
||||
dialogShortcutBinding!!.imageScaleSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
preferences.edit {
|
||||
putBoolean(
|
||||
"shouldStretchIcon",
|
||||
isChecked
|
||||
)
|
||||
}
|
||||
refreshShortcutDialogIcon()
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.create_shortcut)
|
||||
.setView(dialogShortcutBinding!!.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val shortcutName = dialogShortcutBinding!!.shortcutNameInput.text.toString()
|
||||
if (shortcutName.isEmpty()) {
|
||||
Toast.makeText(context, R.string.shortcut_name_empty, Toast.LENGTH_LONG).show()
|
||||
return@setPositiveButton
|
||||
}
|
||||
val iconBitmap = (dialogShortcutBinding!!.shortcutIcon.drawable as BitmapDrawable).bitmap
|
||||
val shortcutManager = activity.getSystemService(ShortcutManager::class.java)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val icon = Icon.createWithBitmap(iconBitmap)
|
||||
val shortcut = ShortcutInfo.Builder(context, shortcutName)
|
||||
.setShortLabel(shortcutName)
|
||||
.setIcon(icon)
|
||||
.setIntent(game.launchIntent.apply {
|
||||
putExtra("launchedFromShortcut", true)
|
||||
})
|
||||
.build()
|
||||
|
||||
shortcutManager?.requestPinShortcut(shortcut, null)
|
||||
imagePath = null
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
imagePath = null
|
||||
}
|
||||
.show()
|
||||
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.cheats).setOnClickListener {
|
||||
|
@ -245,6 +421,14 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.menu_button_open).setOnClickListener {
|
||||
showOpenContextMenu(it, game)
|
||||
}
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.menu_button_uninstall).setOnClickListener {
|
||||
showUninstallContextMenu(it, game, bottomSheetDialog)
|
||||
}
|
||||
|
||||
val bottomSheetBehavior = bottomSheetDialog.getBehavior()
|
||||
bottomSheetBehavior.skipCollapsed = true
|
||||
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
@ -252,6 +436,47 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||
bottomSheetDialog.show()
|
||||
}
|
||||
|
||||
private fun refreshShortcutDialogIcon() {
|
||||
if (imagePath != null) {
|
||||
val originalBitmap = BitmapFactory.decodeStream(
|
||||
CitraApplication.appContext.contentResolver.openInputStream(
|
||||
imagePath!!.toUri()
|
||||
)
|
||||
)
|
||||
val scaledBitmap = {
|
||||
val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
if (preferences.getBoolean("shouldStretchIcon", true)) {
|
||||
// stretch to fit
|
||||
originalBitmap.scale(108, 108)
|
||||
} else {
|
||||
// Zoom in to fit the bitmap while keeping the aspect ratio
|
||||
val width = originalBitmap.width
|
||||
val height = originalBitmap.height
|
||||
val targetSize = 108
|
||||
|
||||
if (width > height) {
|
||||
// Landscape orientation
|
||||
val scaleFactor = targetSize.toFloat() / height
|
||||
val scaledWidth = (width * scaleFactor).toInt()
|
||||
val scaledBmp = originalBitmap.scale(scaledWidth, targetSize)
|
||||
|
||||
val startX = (scaledWidth - targetSize) / 2
|
||||
Bitmap.createBitmap(scaledBmp, startX, 0, targetSize, targetSize)
|
||||
} else {
|
||||
val scaleFactor = targetSize.toFloat() / width
|
||||
val scaledHeight = (height * scaleFactor).toInt()
|
||||
val scaledBmp = originalBitmap.scale(targetSize, scaledHeight)
|
||||
|
||||
val startY = (scaledHeight - targetSize) / 2
|
||||
Bitmap.createBitmap(scaledBmp, 0, startY, targetSize, targetSize)
|
||||
}
|
||||
}
|
||||
}()
|
||||
dialogShortcutBinding!!.shortcutIcon.setImageBitmap(scaledBitmap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGame(extension: String): Boolean {
|
||||
return Game.badExtensions.stream()
|
||||
.noneMatch { extension == it.lowercase() }
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
|
@ -14,9 +15,11 @@ import androidx.core.content.res.ResourcesCompat
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.citra.citra_emu.databinding.PageSetupBinding
|
||||
import org.citra.citra_emu.model.ButtonState
|
||||
import org.citra.citra_emu.model.PageState
|
||||
import org.citra.citra_emu.model.SetupCallback
|
||||
import org.citra.citra_emu.model.SetupPage
|
||||
import org.citra.citra_emu.model.StepState
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.utils.ViewUtils
|
||||
|
||||
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
|
||||
|
@ -42,8 +45,40 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
|
|||
fun bind(page: SetupPage) {
|
||||
this.page = page
|
||||
|
||||
if (page.stepCompleted.invoke() == StepState.STEP_COMPLETE) {
|
||||
onStepCompleted()
|
||||
if (page.pageSteps.invoke() == PageState.PAGE_STEPS_COMPLETE) {
|
||||
onStepCompleted(0, pageFullyCompleted = true)
|
||||
}
|
||||
|
||||
if (page.pageButtons != null && page.pageSteps.invoke() != PageState.PAGE_STEPS_COMPLETE) {
|
||||
for (pageButton in page.pageButtons) {
|
||||
val pageButtonView = LayoutInflater.from(activity)
|
||||
.inflate(
|
||||
R.layout.page_button,
|
||||
binding.pageButtonContainer,
|
||||
false
|
||||
) as MaterialButton
|
||||
|
||||
pageButtonView.apply {
|
||||
id = pageButton.titleId
|
||||
icon = ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
pageButton.iconId,
|
||||
activity.theme
|
||||
)
|
||||
text = activity.resources.getString(pageButton.titleId)
|
||||
}
|
||||
|
||||
pageButtonView.setOnClickListener {
|
||||
pageButton.buttonAction.invoke(this@SetupPageViewHolder)
|
||||
}
|
||||
|
||||
binding.pageButtonContainer.addView(pageButtonView)
|
||||
|
||||
// Disable buton add if its already completed
|
||||
if (pageButton.buttonState.invoke() == ButtonState.BUTTON_ACTION_COMPLETE) {
|
||||
onStepCompleted(pageButton.titleId, pageFullyCompleted = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
|
@ -57,31 +92,26 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
|
|||
binding.textDescription.text =
|
||||
Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
|
||||
binding.textDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
binding.buttonAction.apply {
|
||||
text = activity.resources.getString(page.buttonTextId)
|
||||
if (page.buttonIconId != 0) {
|
||||
icon = ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
page.buttonIconId,
|
||||
activity.theme
|
||||
)
|
||||
}
|
||||
iconGravity =
|
||||
if (page.leftAlignedIcon) {
|
||||
MaterialButton.ICON_GRAVITY_START
|
||||
} else {
|
||||
MaterialButton.ICON_GRAVITY_END
|
||||
}
|
||||
setOnClickListener {
|
||||
page.buttonAction.invoke(this@SetupPageViewHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStepCompleted() {
|
||||
ViewUtils.hideView(binding.buttonAction, 200)
|
||||
ViewUtils.showView(binding.textConfirmation, 200)
|
||||
override fun onStepCompleted(pageButtonId: Int, pageFullyCompleted: Boolean) {
|
||||
val button = binding.pageButtonContainer.findViewById<MaterialButton>(pageButtonId)
|
||||
|
||||
if (pageFullyCompleted) {
|
||||
ViewUtils.hideView(binding.pageButtonContainer, 200)
|
||||
ViewUtils.showView(binding.textConfirmation, 200)
|
||||
}
|
||||
|
||||
if (button != null) {
|
||||
button.isEnabled = false
|
||||
button.animate()
|
||||
.alpha(0.38f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
button.setTextColor(button.context.getColor(com.google.android.material.R.color.material_on_surface_disabled))
|
||||
button.iconTint =
|
||||
ColorStateList.valueOf(button.context.getColor(com.google.android.material.R.color.material_on_surface_disabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -68,4 +68,13 @@ class ScreenAdjustmentUtil(
|
|||
settings.saveSetting(IntSetting.ORIENTATION_OPTION, SettingsFile.FILE_NAME_CONFIG)
|
||||
activity.requestedOrientation = orientationOption
|
||||
}
|
||||
|
||||
fun toggleScreenUpright() {
|
||||
val uprightBoolean = BooleanSetting.UPRIGHT_SCREEN.boolean
|
||||
BooleanSetting.UPRIGHT_SCREEN.boolean = !uprightBoolean
|
||||
settings.saveSetting(BooleanSetting.UPRIGHT_SCREEN, SettingsFile.FILE_NAME_CONFIG)
|
||||
NativeLibrary.reloadSettings()
|
||||
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -41,7 +41,8 @@ enum class SmallScreenPosition(val int: Int) {
|
|||
enum class PortraitScreenLayout(val int: Int) {
|
||||
// These must match what is defined in src/common/settings.h
|
||||
TOP_FULL_WIDTH(0),
|
||||
CUSTOM_PORTRAIT_LAYOUT(1);
|
||||
CUSTOM_PORTRAIT_LAYOUT(1),
|
||||
ORIGINAL(2);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): PortraitScreenLayout {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -10,5 +10,6 @@ enum class Hotkey(val button: Int) {
|
|||
CLOSE_GAME(10003),
|
||||
PAUSE_OR_RESUME(10004),
|
||||
QUICKSAVE(10005),
|
||||
QUICKLOAD(10006);
|
||||
QUICKLOAD(10006),
|
||||
TURBO_LIMIT(10007);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -9,11 +9,15 @@ import android.widget.Toast
|
|||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.utils.EmulationLifecycleUtil
|
||||
import org.citra.citra_emu.utils.TurboHelper
|
||||
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||
|
||||
class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil, private val context: Context) {
|
||||
class HotkeyUtility(
|
||||
private val screenAdjustmentUtil: ScreenAdjustmentUtil,
|
||||
private val context: Context) {
|
||||
|
||||
val hotkeyButtons = Hotkey.entries.map { it.button }
|
||||
private val hotkeyButtons = Hotkey.entries.map { it.button }
|
||||
var HotkeyIsPressed = false
|
||||
|
||||
fun handleHotkey(bindedButton: Int): Boolean {
|
||||
if(hotkeyButtons.contains(bindedButton)) {
|
||||
|
@ -22,16 +26,17 @@ class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil, priv
|
|||
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
|
||||
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
|
||||
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
|
||||
Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true)
|
||||
Hotkey.QUICKSAVE.button -> {
|
||||
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.quicksave_saving),
|
||||
context.getString(R.string.saving),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Hotkey.QUICKLOAD.button -> {
|
||||
val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
|
||||
val stringRes = if(wasLoaded) {
|
||||
R.string.quickload_loading
|
||||
R.string.loading
|
||||
} else {
|
||||
R.string.quickload_not_found
|
||||
}
|
||||
|
@ -41,6 +46,7 @@ class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil, priv
|
|||
}
|
||||
else -> {}
|
||||
}
|
||||
HotkeyIsPressed = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -9,16 +9,46 @@ enum class BooleanSetting(
|
|||
override val section: String,
|
||||
override val defaultValue: Boolean
|
||||
) : AbstractBooleanSetting {
|
||||
EXPAND_TO_CUTOUT_AREA("expand_to_cutout_area", Settings.SECTION_LAYOUT, false),
|
||||
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
|
||||
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
|
||||
DISABLE_SPIRV_OPTIMIZER("disable_spirv_optimizer", Settings.SECTION_RENDERER, true),
|
||||
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
|
||||
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
|
||||
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false),
|
||||
INSTANT_DEBUG_LOG("instant_debug_log", Settings.SECTION_DEBUG, false),
|
||||
ENABLE_RPC_SERVER("enable_rpc_server", Settings.SECTION_DEBUG, false),
|
||||
CUSTOM_LAYOUT("custom_layout",Settings.SECTION_LAYOUT,false),
|
||||
OVERLAY_SHOW_FPS("overlay_show_fps", Settings.SECTION_LAYOUT, true),
|
||||
OVERLAY_SHOW_FRAMETIME("overlay_show_frame_time", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_SPEED("overlay_show_speed", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_APP_RAM_USAGE("overlay_show_app_ram_usage", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_AVAILABLE_RAM("overlay_show_available_ram", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_BATTERY_TEMP("overlay_show_battery_temp", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_BACKGROUND("overlay_background", Settings.SECTION_LAYOUT, false),
|
||||
DELAY_START_LLE_MODULES("delay_start_for_lle_modules", Settings.SECTION_DEBUG, true),
|
||||
DETERMINISTIC_ASYNC_OPERATIONS("deterministic_async_operations", Settings.SECTION_DEBUG, false),
|
||||
REQUIRED_ONLINE_LLE_MODULES("enable_required_online_lle_modules", Settings.SECTION_SYSTEM, false);
|
||||
REQUIRED_ONLINE_LLE_MODULES("enable_required_online_lle_modules", Settings.SECTION_SYSTEM, false),
|
||||
LLE_APPLETS("lle_applets", Settings.SECTION_SYSTEM, false),
|
||||
NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, true),
|
||||
LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, true),
|
||||
SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, false),
|
||||
DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, true),
|
||||
DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, false),
|
||||
CUSTOM_TEXTURES("custom_textures", Settings.SECTION_UTILITY, false),
|
||||
ASYNC_CUSTOM_LOADING("async_custom_loading", Settings.SECTION_UTILITY, true),
|
||||
PRELOAD_TEXTURES("preload_textures", Settings.SECTION_UTILITY, false),
|
||||
ENABLE_AUDIO_STRETCHING("enable_audio_stretching", Settings.SECTION_AUDIO, true),
|
||||
ENABLE_REALTIME_AUDIO("enable_realtime_audio", Settings.SECTION_AUDIO, false),
|
||||
CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, true),
|
||||
HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, true),
|
||||
SHADER_JIT("use_shader_jit", Settings.SECTION_RENDERER, true),
|
||||
VSYNC("use_vsync_new", Settings.SECTION_RENDERER, true),
|
||||
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, true),
|
||||
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, false),
|
||||
DISABLE_RIGHT_EYE_RENDER("disable_right_eye_render", Settings.SECTION_RENDERER, false),
|
||||
USE_ARTIC_BASE_CONTROLLER("use_artic_base_controller", Settings.SECTION_CONTROLS, false),
|
||||
UPRIGHT_SCREEN("upright_screen", Settings.SECTION_LAYOUT, false);
|
||||
|
||||
override var boolean: Boolean = defaultValue
|
||||
|
||||
|
@ -43,6 +73,14 @@ enum class BooleanSetting(
|
|||
DELAY_START_LLE_MODULES,
|
||||
DETERMINISTIC_ASYNC_OPERATIONS,
|
||||
REQUIRED_ONLINE_LLE_MODULES,
|
||||
NEW_3DS,
|
||||
LLE_APPLETS,
|
||||
VSYNC,
|
||||
DEBUG_RENDERER,
|
||||
CPU_JIT,
|
||||
ASYNC_CUSTOM_LOADING,
|
||||
SHADERS_ACCURATE_MUL,
|
||||
USE_ARTIC_BASE_CONTROLLER
|
||||
)
|
||||
|
||||
fun from(key: String): BooleanSetting? =
|
||||
|
|
|
@ -33,6 +33,7 @@ enum class IntSetting(
|
|||
LANDSCAPE_BOTTOM_Y("custom_bottom_y",Settings.SECTION_LAYOUT,480),
|
||||
LANDSCAPE_BOTTOM_WIDTH("custom_bottom_width",Settings.SECTION_LAYOUT,640),
|
||||
LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480),
|
||||
SCREEN_GAP("screen_gap",Settings.SECTION_LAYOUT,0),
|
||||
PORTRAIT_SCREEN_LAYOUT("portrait_layout_option",Settings.SECTION_LAYOUT,0),
|
||||
PORTRAIT_TOP_X("custom_portrait_top_x",Settings.SECTION_LAYOUT,0),
|
||||
PORTRAIT_TOP_Y("custom_portrait_top_y",Settings.SECTION_LAYOUT,0),
|
||||
|
@ -43,29 +44,16 @@ enum class IntSetting(
|
|||
PORTRAIT_BOTTOM_WIDTH("custom_portrait_bottom_width",Settings.SECTION_LAYOUT,640),
|
||||
PORTRAIT_BOTTOM_HEIGHT("custom_portrait_bottom_height",Settings.SECTION_LAYOUT,480),
|
||||
AUDIO_INPUT_TYPE("input_type", Settings.SECTION_AUDIO, 0),
|
||||
NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, 1),
|
||||
LLE_APPLETS("lle_applets", Settings.SECTION_SYSTEM, 1),
|
||||
CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100),
|
||||
LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, 1),
|
||||
SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, 0),
|
||||
DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, 1),
|
||||
DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, 0),
|
||||
CUSTOM_TEXTURES("custom_textures", Settings.SECTION_UTILITY, 0),
|
||||
ASYNC_CUSTOM_LOADING("async_custom_loading", Settings.SECTION_UTILITY, 1),
|
||||
PRELOAD_TEXTURES("preload_textures", Settings.SECTION_UTILITY, 0),
|
||||
ENABLE_AUDIO_STRETCHING("enable_audio_stretching", Settings.SECTION_AUDIO, 1),
|
||||
ENABLE_REALTIME_AUDIO("enable_realtime_audio", Settings.SECTION_AUDIO, 0),
|
||||
CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, 1),
|
||||
HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, 1),
|
||||
VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1),
|
||||
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0),
|
||||
TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0),
|
||||
TEXTURE_SAMPLING("texture_sampling", Settings.SECTION_RENDERER, 0),
|
||||
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1),
|
||||
DELAY_RENDER_THREAD_US("delay_game_render_thread_us", Settings.SECTION_RENDERER, 0),
|
||||
USE_ARTIC_BASE_CONTROLLER("use_artic_base_controller", Settings.SECTION_CONTROLS, 0),
|
||||
ORIENTATION_OPTION("screen_orientation", Settings.SECTION_LAYOUT, 2),
|
||||
DISABLE_RIGHT_EYE_RENDER("disable_right_eye_render", Settings.SECTION_RENDERER, 0);
|
||||
TURBO_LIMIT("turbo_limit", Settings.SECTION_CORE, 200),
|
||||
PERFORMANCE_OVERLAY_POSITION("performance_overlay_position", Settings.SECTION_LAYOUT, 0),
|
||||
ASPECT_RATIO("aspect_ratio", Settings.SECTION_LAYOUT, 0);
|
||||
|
||||
override var int: Int = defaultValue
|
||||
|
||||
override val valueAsString: String
|
||||
|
@ -85,16 +73,8 @@ enum class IntSetting(
|
|||
private val NOT_RUNTIME_EDITABLE = listOf(
|
||||
EMULATED_REGION,
|
||||
INIT_CLOCK,
|
||||
NEW_3DS,
|
||||
LLE_APPLETS,
|
||||
GRAPHICS_API,
|
||||
VSYNC,
|
||||
DEBUG_RENDERER,
|
||||
CPU_JIT,
|
||||
ASYNC_CUSTOM_LOADING,
|
||||
AUDIO_INPUT_TYPE,
|
||||
USE_ARTIC_BASE_CONTROLLER,
|
||||
SHADERS_ACCURATE_MUL,
|
||||
)
|
||||
|
||||
fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -111,6 +111,7 @@ class Settings {
|
|||
const val SECTION_THEME = "Theme"
|
||||
const val SECTION_CUSTOM_LANDSCAPE = "Custom Landscape Layout"
|
||||
const val SECTION_CUSTOM_PORTRAIT = "Custom Portrait Layout"
|
||||
const val SECTION_PERFORMANCE_OVERLAY = "Performance Overlay"
|
||||
|
||||
const val KEY_BUTTON_A = "button_a"
|
||||
const val KEY_BUTTON_B = "button_b"
|
||||
|
@ -139,6 +140,7 @@ class Settings {
|
|||
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game"
|
||||
const val HOTKEY_QUICKSAVE = "hotkey_quickload"
|
||||
const val HOTKEY_QUICKlOAD = "hotkey_quickpause"
|
||||
const val HOTKEY_TURBO_LIMIT = "hotkey_turbo_limit"
|
||||
|
||||
val buttonKeys = listOf(
|
||||
KEY_BUTTON_A,
|
||||
|
@ -204,7 +206,8 @@ class Settings {
|
|||
HOTKEY_CLOSE_GAME,
|
||||
HOTKEY_PAUSE_OR_RESUME,
|
||||
HOTKEY_QUICKSAVE,
|
||||
HOTKEY_QUICKlOAD
|
||||
HOTKEY_QUICKlOAD,
|
||||
HOTKEY_TURBO_LIMIT
|
||||
)
|
||||
val hotkeyTitles = listOf(
|
||||
R.string.emulation_swap_screens,
|
||||
|
@ -213,6 +216,7 @@ class Settings {
|
|||
R.string.emulation_toggle_pause,
|
||||
R.string.emulation_quicksave,
|
||||
R.string.emulation_quickload,
|
||||
R.string.turbo_limit_hotkey
|
||||
)
|
||||
|
||||
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
||||
|
|
|
@ -12,7 +12,8 @@ class DateTimeSetting(
|
|||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val key: String? = null,
|
||||
private val defaultValue: String? = null
|
||||
private val defaultValue: String? = null,
|
||||
override var isEnabled: Boolean = true
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_DATETIME_SETTING
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -133,6 +133,7 @@ class InputBindingSetting(
|
|||
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button
|
||||
Settings.HOTKEY_QUICKSAVE -> Hotkey.QUICKSAVE.button
|
||||
Settings.HOTKEY_QUICKlOAD -> Hotkey.QUICKLOAD.button
|
||||
Settings.HOTKEY_TURBO_LIMIT -> Hotkey.TURBO_LIMIT.button
|
||||
else -> -1
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright Citra Emulator Project / Lime3DS Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -28,6 +28,13 @@ abstract class SettingsItem(
|
|||
return setting?.isRuntimeEditable ?: false
|
||||
}
|
||||
|
||||
open var isEnabled: Boolean = true
|
||||
|
||||
val isActive: Boolean
|
||||
get() {
|
||||
return this.isEditable && this.isEnabled
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_HEADER = 0
|
||||
const val TYPE_SWITCH = 1
|
||||
|
|
|
@ -15,7 +15,8 @@ class SingleChoiceSetting(
|
|||
val choicesId: Int,
|
||||
val valuesId: Int,
|
||||
val key: String? = null,
|
||||
val defaultValue: Int? = null
|
||||
val defaultValue: Int? = null,
|
||||
override var isEnabled: Boolean = true
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SINGLE_CHOICE
|
||||
|
||||
|
|
|
@ -20,7 +20,8 @@ class SliderSetting(
|
|||
val max: Int,
|
||||
val units: String,
|
||||
val key: String? = null,
|
||||
val defaultValue: Float? = null
|
||||
val defaultValue: Float? = null,
|
||||
override var isEnabled: Boolean = true
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SLIDER
|
||||
val selectedFloat: Float
|
||||
|
|
|
@ -12,7 +12,8 @@ class StringInputSetting(
|
|||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val defaultValue: String,
|
||||
val characterLimit: Int = 0
|
||||
val characterLimit: Int = 0,
|
||||
override var isEnabled: Boolean = true
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_STRING_INPUT
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ class StringSingleChoiceSetting(
|
|||
val choices: Array<String>,
|
||||
val values: Array<String>?,
|
||||
val key: String? = null,
|
||||
private val defaultValue: String? = null
|
||||
private val defaultValue: String? = null,
|
||||
override var isEnabled: Boolean = true
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_STRING_SINGLE_CHOICE
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -9,53 +9,31 @@ import org.citra.citra_emu.features.settings.model.AbstractIntSetting
|
|||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
|
||||
class SwitchSetting(
|
||||
setting: AbstractSetting,
|
||||
setting: AbstractBooleanSetting,
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val key: String? = null,
|
||||
val defaultValue: Any? = null
|
||||
val defaultValue: Boolean = false,
|
||||
override var isEnabled: Boolean = true
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SWITCH
|
||||
|
||||
val isChecked: Boolean
|
||||
get() {
|
||||
if (setting == null) {
|
||||
return defaultValue as Boolean
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Try integer setting
|
||||
try {
|
||||
val setting = setting as AbstractIntSetting
|
||||
return setting.int == 1
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
|
||||
// Try boolean setting
|
||||
try {
|
||||
val setting = setting as AbstractBooleanSetting
|
||||
return setting.boolean
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
return defaultValue as Boolean
|
||||
val setting = setting as AbstractBooleanSetting
|
||||
return setting.boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing boolean. If that boolean was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
* Write a value to the backing boolean.
|
||||
*
|
||||
* @param checked Pretty self explanatory.
|
||||
* @return the existing setting with the new value applied.
|
||||
*/
|
||||
fun setChecked(checked: Boolean): AbstractSetting {
|
||||
// Try integer setting
|
||||
try {
|
||||
val setting = setting as AbstractIntSetting
|
||||
setting.int = if (checked) 1 else 0
|
||||
return setting
|
||||
} catch (_: ClassCastException) {
|
||||
}
|
||||
|
||||
// Try boolean setting
|
||||
fun setChecked(checked: Boolean): AbstractBooleanSetting {
|
||||
val setting = setting as AbstractBooleanSetting
|
||||
setting.boolean = checked
|
||||
return setting
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.citra.citra_emu.features.settings.model.Settings
|
|||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.TurboHelper
|
||||
|
||||
class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
|
||||
val settings: Settings get() = activityView.settings
|
||||
|
@ -66,6 +67,7 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView)
|
|||
//added to ensure that layout changes take effect as soon as settings window closes
|
||||
NativeLibrary.reloadSettings()
|
||||
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)
|
||||
TurboHelper.reloadTurbo(false) // TODO: Can this go somewhere else? -OS
|
||||
}
|
||||
NativeLibrary.reloadSettings()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ package org.citra.citra_emu.features.settings.ui
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.icu.util.Calendar
|
||||
import android.icu.util.TimeZone
|
||||
import android.text.Editable
|
||||
|
@ -17,11 +16,11 @@ import android.text.TextWatcher
|
|||
import android.text.format.DateFormat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
@ -66,7 +65,6 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHold
|
|||
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||
import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.NumberFormatException
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
@ -153,15 +151,71 @@ class SettingsAdapter(
|
|||
return getItem(position)?.type ?: -1
|
||||
}
|
||||
|
||||
fun setSettingsList(settings: ArrayList<SettingsItem>?) {
|
||||
this.settings = settings ?: arrayListOf()
|
||||
notifyDataSetChanged()
|
||||
fun setSettingsList(newSettings: ArrayList<SettingsItem>?) {
|
||||
if (settings == null) {
|
||||
settings = newSettings ?: arrayListOf()
|
||||
notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
|
||||
val oldSettings = settings
|
||||
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||
override fun getOldListSize() = oldSettings?.size ?: 0
|
||||
override fun getNewListSize() = newSettings?.size ?: 0
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldSettings?.get(oldItemPosition)?.setting
|
||||
val newItem = newSettings?.get(newItemPosition)?.setting
|
||||
return oldItem?.key == newItem?.key
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldSettings?.get(oldItemPosition)
|
||||
val newItem = newSettings?.get(newItemPosition)
|
||||
|
||||
if (oldItem == null || newItem == null || oldItem.type != newItem.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
return when (oldItem.type) {
|
||||
SettingsItem.TYPE_SLIDER -> {
|
||||
(oldItem as SliderSetting).isEnabled == (newItem as SliderSetting).isEnabled
|
||||
}
|
||||
SettingsItem.TYPE_SWITCH -> {
|
||||
(oldItem as SwitchSetting).isEnabled == (newItem as SwitchSetting).isEnabled
|
||||
}
|
||||
SettingsItem.TYPE_SINGLE_CHOICE -> {
|
||||
(oldItem as SingleChoiceSetting).isEnabled == (newItem as SingleChoiceSetting).isEnabled
|
||||
}
|
||||
SettingsItem.TYPE_DATETIME_SETTING -> {
|
||||
(oldItem as DateTimeSetting).isEnabled == (newItem as DateTimeSetting).isEnabled
|
||||
}
|
||||
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
|
||||
(oldItem as StringSingleChoiceSetting).isEnabled == (newItem as StringSingleChoiceSetting).isEnabled
|
||||
}
|
||||
SettingsItem.TYPE_STRING_INPUT -> {
|
||||
(oldItem as StringInputSetting).isEnabled == (newItem as StringInputSetting).isEnabled
|
||||
}
|
||||
else -> {
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
settings = newSettings ?: arrayListOf()
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
|
||||
val setting = item.setChecked(checked)
|
||||
fragmentView.putSetting(setting)
|
||||
fragmentView.onSettingChanged()
|
||||
|
||||
// If statement is required otherwise the app will crash on activity recreate ex. theme settings
|
||||
if (fragmentView.activityView != null)
|
||||
// Reload the settings list to update the UI
|
||||
fragmentView.loadSettingsList()
|
||||
}
|
||||
|
||||
private fun onSingleChoiceClick(item: SingleChoiceSetting) {
|
||||
|
@ -247,6 +301,7 @@ class SettingsAdapter(
|
|||
notifyItemChanged(clickedPosition)
|
||||
val setting = item.setSelectedValue(rtcString)
|
||||
fragmentView.putSetting(setting)
|
||||
fragmentView.loadSettingsList()
|
||||
clickedItem = null
|
||||
}
|
||||
datePicker.show(
|
||||
|
@ -402,6 +457,7 @@ class SettingsAdapter(
|
|||
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
|
||||
}
|
||||
fragmentView?.putSetting(setting)
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
|
@ -425,6 +481,7 @@ class SettingsAdapter(
|
|||
}
|
||||
|
||||
fragmentView?.putSetting(setting)
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
|
@ -447,6 +504,7 @@ class SettingsAdapter(
|
|||
fragmentView?.putSetting(setting)
|
||||
}
|
||||
}
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
|
@ -459,6 +517,7 @@ class SettingsAdapter(
|
|||
}
|
||||
val setting = it.setSelectedValue(textInputValue ?: "")
|
||||
fragmentView?.putSetting(setting)
|
||||
fragmentView.loadSettingsList()
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
|
@ -488,6 +547,7 @@ class SettingsAdapter(
|
|||
}
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.loadSettingsList()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
@ -495,10 +555,19 @@ class SettingsAdapter(
|
|||
return true
|
||||
}
|
||||
|
||||
fun onClickDisabledSetting() {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.setting_not_editable,
|
||||
fun onClickDisabledSetting(isRuntimeDisabled: Boolean) {
|
||||
val titleId = if (isRuntimeDisabled)
|
||||
R.string.setting_not_editable
|
||||
else
|
||||
R.string.setting_disabled
|
||||
val messageId = if (isRuntimeDisabled)
|
||||
R.string.setting_not_editable_description
|
||||
else
|
||||
R.string.setting_disabled_description
|
||||
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId,
|
||||
messageId
|
||||
).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ import org.citra.citra_emu.utils.BirthdayMonth
|
|||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
|
||||
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
|
||||
private var menuTag: String? = null
|
||||
|
@ -102,6 +103,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
Settings.SECTION_THEME -> addThemeSettings(sl)
|
||||
Settings.SECTION_CUSTOM_LANDSCAPE -> addCustomLandscapeSettings(sl)
|
||||
Settings.SECTION_CUSTOM_PORTRAIT -> addCustomPortraitSettings(sl)
|
||||
Settings.SECTION_PERFORMANCE_OVERLAY -> addPerformanceOverlaySettings(sl)
|
||||
else -> {
|
||||
fragmentView.showToastMessage("Unimplemented menu", false)
|
||||
return
|
||||
|
@ -218,11 +220,11 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
sl.apply {
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.USE_FRAME_LIMIT,
|
||||
BooleanSetting.USE_FRAME_LIMIT,
|
||||
R.string.frame_limit_enable,
|
||||
R.string.frame_limit_enable_description,
|
||||
IntSetting.USE_FRAME_LIMIT.key,
|
||||
IntSetting.USE_FRAME_LIMIT.defaultValue
|
||||
BooleanSetting.USE_FRAME_LIMIT.key,
|
||||
BooleanSetting.USE_FRAME_LIMIT.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
@ -237,6 +239,18 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
IntSetting.FRAME_LIMIT.defaultValue.toFloat()
|
||||
)
|
||||
)
|
||||
add(
|
||||
SliderSetting(
|
||||
IntSetting.TURBO_LIMIT,
|
||||
R.string.turbo_limit,
|
||||
R.string.turbo_limit_description,
|
||||
100,
|
||||
400,
|
||||
"%",
|
||||
IntSetting.TURBO_LIMIT.key,
|
||||
IntSetting.TURBO_LIMIT.defaultValue.toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,20 +295,20 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
add(HeaderSetting(R.string.emulation_settings))
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.NEW_3DS,
|
||||
BooleanSetting.NEW_3DS,
|
||||
R.string.new_3ds,
|
||||
0,
|
||||
IntSetting.NEW_3DS.key,
|
||||
IntSetting.NEW_3DS.defaultValue
|
||||
BooleanSetting.NEW_3DS.key,
|
||||
BooleanSetting.NEW_3DS.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.LLE_APPLETS,
|
||||
BooleanSetting.LLE_APPLETS,
|
||||
R.string.lle_applets,
|
||||
0,
|
||||
IntSetting.LLE_APPLETS.key,
|
||||
IntSetting.LLE_APPLETS.defaultValue
|
||||
BooleanSetting.LLE_APPLETS.key,
|
||||
BooleanSetting.LLE_APPLETS.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
@ -526,7 +540,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
DateTimeSetting(
|
||||
StringSetting.INIT_TIME,
|
||||
R.string.simulated_clock,
|
||||
R.string.init_time_description,
|
||||
R.string.simulated_clock_description,
|
||||
StringSetting.INIT_TIME.key,
|
||||
StringSetting.INIT_TIME.defaultValue
|
||||
)
|
||||
|
@ -774,11 +788,11 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
add(HeaderSetting(R.string.miscellaneous))
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.USE_ARTIC_BASE_CONTROLLER,
|
||||
BooleanSetting.USE_ARTIC_BASE_CONTROLLER,
|
||||
R.string.use_artic_base_controller,
|
||||
R.string.use_artic_base_controller_description,
|
||||
IntSetting.USE_ARTIC_BASE_CONTROLLER.key,
|
||||
IntSetting.USE_ARTIC_BASE_CONTROLLER.defaultValue
|
||||
BooleanSetting.USE_ARTIC_BASE_CONTROLLER.key,
|
||||
BooleanSetting.USE_ARTIC_BASE_CONTROLLER.defaultValue
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -825,6 +839,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
BooleanSetting.SPIRV_SHADER_GEN.defaultValue,
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.DISABLE_SPIRV_OPTIMIZER,
|
||||
R.string.disable_spirv_optimizer,
|
||||
R.string.disable_spirv_optimizer_description,
|
||||
BooleanSetting.DISABLE_SPIRV_OPTIMIZER.key,
|
||||
BooleanSetting.DISABLE_SPIRV_OPTIMIZER.defaultValue,
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.ASYNC_SHADERS,
|
||||
|
@ -847,29 +870,29 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.LINEAR_FILTERING,
|
||||
BooleanSetting.LINEAR_FILTERING,
|
||||
R.string.linear_filtering,
|
||||
R.string.linear_filtering_description,
|
||||
IntSetting.LINEAR_FILTERING.key,
|
||||
IntSetting.LINEAR_FILTERING.defaultValue
|
||||
BooleanSetting.LINEAR_FILTERING.key,
|
||||
BooleanSetting.LINEAR_FILTERING.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.SHADERS_ACCURATE_MUL,
|
||||
BooleanSetting.SHADERS_ACCURATE_MUL,
|
||||
R.string.shaders_accurate_mul,
|
||||
R.string.shaders_accurate_mul_description,
|
||||
IntSetting.SHADERS_ACCURATE_MUL.key,
|
||||
IntSetting.SHADERS_ACCURATE_MUL.defaultValue
|
||||
BooleanSetting.SHADERS_ACCURATE_MUL.key,
|
||||
BooleanSetting.SHADERS_ACCURATE_MUL.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.DISK_SHADER_CACHE,
|
||||
BooleanSetting.DISK_SHADER_CACHE,
|
||||
R.string.use_disk_shader_cache,
|
||||
R.string.use_disk_shader_cache_description,
|
||||
IntSetting.DISK_SHADER_CACHE.key,
|
||||
IntSetting.DISK_SHADER_CACHE.defaultValue
|
||||
BooleanSetting.DISK_SHADER_CACHE.key,
|
||||
BooleanSetting.DISK_SHADER_CACHE.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
@ -914,7 +937,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.factor3d,
|
||||
R.string.factor3d_description,
|
||||
0,
|
||||
100,
|
||||
255,
|
||||
"%",
|
||||
IntSetting.STEREOSCOPIC_3D_DEPTH.key,
|
||||
IntSetting.STEREOSCOPIC_3D_DEPTH.defaultValue.toFloat()
|
||||
|
@ -922,11 +945,11 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.DISABLE_RIGHT_EYE_RENDER,
|
||||
BooleanSetting.DISABLE_RIGHT_EYE_RENDER,
|
||||
R.string.disable_right_eye_render,
|
||||
R.string.disable_right_eye_render_description,
|
||||
IntSetting.DISABLE_RIGHT_EYE_RENDER.key,
|
||||
IntSetting.DISABLE_RIGHT_EYE_RENDER.defaultValue
|
||||
BooleanSetting.DISABLE_RIGHT_EYE_RENDER.key,
|
||||
BooleanSetting.DISABLE_RIGHT_EYE_RENDER.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -971,29 +994,29 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
add(HeaderSetting(R.string.utility))
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.DUMP_TEXTURES,
|
||||
BooleanSetting.DUMP_TEXTURES,
|
||||
R.string.dump_textures,
|
||||
R.string.dump_textures_description,
|
||||
IntSetting.DUMP_TEXTURES.key,
|
||||
IntSetting.DUMP_TEXTURES.defaultValue
|
||||
BooleanSetting.DUMP_TEXTURES.key,
|
||||
BooleanSetting.DUMP_TEXTURES.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.CUSTOM_TEXTURES,
|
||||
BooleanSetting.CUSTOM_TEXTURES,
|
||||
R.string.custom_textures,
|
||||
R.string.custom_textures_description,
|
||||
IntSetting.CUSTOM_TEXTURES.key,
|
||||
IntSetting.CUSTOM_TEXTURES.defaultValue
|
||||
BooleanSetting.CUSTOM_TEXTURES.key,
|
||||
BooleanSetting.CUSTOM_TEXTURES.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.ASYNC_CUSTOM_LOADING,
|
||||
BooleanSetting.ASYNC_CUSTOM_LOADING,
|
||||
R.string.async_custom_loading,
|
||||
R.string.async_custom_loading_description,
|
||||
IntSetting.ASYNC_CUSTOM_LOADING.key,
|
||||
IntSetting.ASYNC_CUSTOM_LOADING.defaultValue
|
||||
BooleanSetting.ASYNC_CUSTOM_LOADING.key,
|
||||
BooleanSetting.ASYNC_CUSTOM_LOADING.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -1038,6 +1061,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
IntSetting.ORIENTATION_OPTION.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.EXPAND_TO_CUTOUT_AREA,
|
||||
R.string.expand_to_cutout_area,
|
||||
R.string.expand_to_cutout_area_description,
|
||||
BooleanSetting.EXPAND_TO_CUTOUT_AREA.key,
|
||||
BooleanSetting.EXPAND_TO_CUTOUT_AREA.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.SCREEN_LAYOUT,
|
||||
|
@ -1049,6 +1081,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
IntSetting.SCREEN_LAYOUT.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.UPRIGHT_SCREEN,
|
||||
R.string.emulation_rotate_upright,
|
||||
0,
|
||||
BooleanSetting.UPRIGHT_SCREEN.key,
|
||||
BooleanSetting.UPRIGHT_SCREEN.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.PORTRAIT_SCREEN_LAYOUT,
|
||||
|
@ -1060,6 +1101,18 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.ASPECT_RATIO,
|
||||
R.string.emulation_aspect_ratio,
|
||||
0,
|
||||
R.array.aspectRatioNames,
|
||||
R.array.aspectRatioValues,
|
||||
IntSetting.ASPECT_RATIO.key,
|
||||
IntSetting.ASPECT_RATIO.defaultValue,
|
||||
isEnabled = IntSetting.SCREEN_LAYOUT.int == 1,
|
||||
)
|
||||
)
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.SMALL_SCREEN_POSITION,
|
||||
|
@ -1071,6 +1124,18 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
IntSetting.SMALL_SCREEN_POSITION.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SliderSetting(
|
||||
IntSetting.SCREEN_GAP,
|
||||
R.string.screen_gap,
|
||||
R.string.screen_gap_description,
|
||||
0,
|
||||
480,
|
||||
"px",
|
||||
IntSetting.SCREEN_GAP.key,
|
||||
IntSetting.SCREEN_GAP.defaultValue.toFloat()
|
||||
)
|
||||
)
|
||||
add(
|
||||
SliderSetting(
|
||||
FloatSetting.LARGE_SCREEN_PROPORTION,
|
||||
|
@ -1083,6 +1148,14 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
FloatSetting.LARGE_SCREEN_PROPORTION.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SubmenuSetting(
|
||||
R.string.performance_overlay_options,
|
||||
R.string.performance_overlay_options_description,
|
||||
R.drawable.ic_stats,
|
||||
Settings.SECTION_PERFORMANCE_OVERLAY
|
||||
)
|
||||
)
|
||||
add(
|
||||
SubmenuSetting(
|
||||
R.string.emulation_landscape_custom_layout,
|
||||
|
@ -1102,6 +1175,116 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
}
|
||||
}
|
||||
|
||||
private fun addPerformanceOverlaySettings(sl: ArrayList<SettingsItem>) {
|
||||
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.performance_overlay_options))
|
||||
sl.apply {
|
||||
|
||||
add(HeaderSetting(R.string.visibility))
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
object : AbstractBooleanSetting {
|
||||
override val key = "EmulationMenuSettings_showPerfPerformanceOverlay"
|
||||
override val section = Settings.SECTION_LAYOUT
|
||||
override val defaultValue = false
|
||||
override var boolean: Boolean
|
||||
get() = EmulationMenuSettings.showPerformanceOverlay
|
||||
set(value) { EmulationMenuSettings.showPerformanceOverlay = value }
|
||||
override val isRuntimeEditable = true
|
||||
override val valueAsString: String get() = boolean.toString()
|
||||
},
|
||||
R.string.performance_overlay_enable,
|
||||
0,
|
||||
"EmulationMenuSettings_showPerfPerformanceOverlay",
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_BACKGROUND,
|
||||
R.string.overlay_background,
|
||||
R.string.overlay_background_description,
|
||||
"overlay_background",
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.PERFORMANCE_OVERLAY_POSITION,
|
||||
R.string.overlay_position,
|
||||
R.string.overlay_position_description,
|
||||
R.array.statsPosition,
|
||||
R.array.statsPositionValues,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
add(HeaderSetting(R.string.information))
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_FPS,
|
||||
R.string.overlay_show_fps,
|
||||
R.string.overlay_show_fps_description,
|
||||
"overlay_show_fps",
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_FRAMETIME,
|
||||
R.string.overlay_show_frametime,
|
||||
R.string.overlay_show_frametime_description,
|
||||
"overlay_show_frame_time",
|
||||
true
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_SPEED,
|
||||
R.string.overlay_show_speed,
|
||||
R.string.overlay_show_speed_description,
|
||||
"overlay_show_speed",
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_APP_RAM_USAGE,
|
||||
R.string.overlay_show_app_ram_usage,
|
||||
R.string.overlay_show_app_ram_usage_description,
|
||||
"overlay_show_app_ram_usage",
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_AVAILABLE_RAM,
|
||||
R.string.overlay_show_available_ram,
|
||||
R.string.overlay_show_available_ram_description,
|
||||
"overlay_show_available_ram",
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_BATTERY_TEMP,
|
||||
R.string.overlay_show_battery_temp,
|
||||
R.string.overlay_show_battery_temp_description,
|
||||
"overlay_show_battery_temp",
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCustomLandscapeSettings(sl: ArrayList<SettingsItem>) {
|
||||
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.emulation_landscape_custom_layout))
|
||||
sl.apply {
|
||||
|
@ -1329,20 +1512,20 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.ENABLE_AUDIO_STRETCHING,
|
||||
BooleanSetting.ENABLE_AUDIO_STRETCHING,
|
||||
R.string.audio_stretch,
|
||||
R.string.audio_stretch_description,
|
||||
IntSetting.ENABLE_AUDIO_STRETCHING.key,
|
||||
IntSetting.ENABLE_AUDIO_STRETCHING.defaultValue
|
||||
BooleanSetting.ENABLE_AUDIO_STRETCHING.key,
|
||||
BooleanSetting.ENABLE_AUDIO_STRETCHING.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.ENABLE_REALTIME_AUDIO,
|
||||
BooleanSetting.ENABLE_REALTIME_AUDIO,
|
||||
R.string.realtime_audio,
|
||||
R.string.realtime_audio_description,
|
||||
IntSetting.ENABLE_REALTIME_AUDIO.key,
|
||||
IntSetting.ENABLE_REALTIME_AUDIO.defaultValue
|
||||
BooleanSetting.ENABLE_REALTIME_AUDIO.key,
|
||||
BooleanSetting.ENABLE_REALTIME_AUDIO.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
@ -1397,38 +1580,47 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.CPU_JIT,
|
||||
BooleanSetting.CPU_JIT,
|
||||
R.string.cpu_jit,
|
||||
R.string.cpu_jit_description,
|
||||
IntSetting.CPU_JIT.key,
|
||||
IntSetting.CPU_JIT.defaultValue
|
||||
BooleanSetting.CPU_JIT.key,
|
||||
BooleanSetting.CPU_JIT.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.HW_SHADER,
|
||||
BooleanSetting.HW_SHADER,
|
||||
R.string.hw_shaders,
|
||||
R.string.hw_shaders_description,
|
||||
IntSetting.HW_SHADER.key,
|
||||
IntSetting.HW_SHADER.defaultValue
|
||||
BooleanSetting.HW_SHADER.key,
|
||||
BooleanSetting.HW_SHADER.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.VSYNC,
|
||||
BooleanSetting.SHADER_JIT,
|
||||
R.string.shader_jit,
|
||||
R.string.shader_jit_description,
|
||||
BooleanSetting.SHADER_JIT.key,
|
||||
BooleanSetting.SHADER_JIT.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.VSYNC,
|
||||
R.string.vsync,
|
||||
R.string.vsync_description,
|
||||
IntSetting.VSYNC.key,
|
||||
IntSetting.VSYNC.defaultValue
|
||||
BooleanSetting.VSYNC.key,
|
||||
BooleanSetting.VSYNC.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
IntSetting.DEBUG_RENDERER,
|
||||
BooleanSetting.DEBUG_RENDERER,
|
||||
R.string.renderer_debug,
|
||||
R.string.renderer_debug_description,
|
||||
IntSetting.DEBUG_RENDERER.key,
|
||||
IntSetting.DEBUG_RENDERER.defaultValue
|
||||
BooleanSetting.DEBUG_RENDERER.key,
|
||||
BooleanSetting.DEBUG_RENDERER.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
@ -1440,6 +1632,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
BooleanSetting.INSTANT_DEBUG_LOG.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.ENABLE_RPC_SERVER,
|
||||
R.string.enable_rpc_server,
|
||||
R.string.enable_rpc_server_desc,
|
||||
BooleanSetting.ENABLE_RPC_SERVER.key,
|
||||
BooleanSetting.ENABLE_RPC_SERVER.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.DELAY_START_LLE_MODULES,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -47,7 +47,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
|||
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
||||
binding.textSettingValue.text = dateFormatter.format(zonedTime)
|
||||
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
binding.textSettingName.alpha = 1f
|
||||
binding.textSettingDescription.alpha = 1f
|
||||
binding.textSettingValue.alpha = 1f
|
||||
|
@ -59,18 +59,18 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
|||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
adapter.onDateTimeClick(setting, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -45,7 +45,7 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter
|
|||
if (setting.isEditable) {
|
||||
adapter.onInputBindingClick(setting, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter
|
|||
if (setting.isEditable) {
|
||||
adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -60,7 +60,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
|||
|
||||
override fun onClick(clicked: View) {
|
||||
if (!setting.isRuntimeRunnable && EmulationActivity.isRunning()) {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(true)
|
||||
} else {
|
||||
setting.runnable.invoke()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -27,7 +27,7 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
|
|||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
binding.textSettingValue.text = getTextSetting()
|
||||
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
binding.textSettingName.alpha = 1f
|
||||
binding.textSettingDescription.alpha = 1f
|
||||
binding.textSettingValue.alpha = 1f
|
||||
|
@ -65,8 +65,8 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
|
|||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
if (!setting.isEditable) {
|
||||
adapter.onClickDisabledSetting()
|
||||
if (!setting.isEditable || !setting.isEnabled) {
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -84,10 +84,10 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
|
|||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -35,7 +35,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
|
|||
else -> "${(setting.setting as AbstractIntSetting).int}${setting.units}"
|
||||
}
|
||||
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
binding.textSettingName.alpha = 1f
|
||||
binding.textSettingDescription.alpha = 1f
|
||||
binding.textSettingValue.alpha = 1f
|
||||
|
@ -47,18 +47,18 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
|
|||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
adapter.onSliderClick(setting, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -25,21 +25,31 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin
|
|||
}
|
||||
binding.textSettingValue.visibility = View.VISIBLE
|
||||
binding.textSettingValue.text = setting.setting?.valueAsString
|
||||
|
||||
if (setting.isActive) {
|
||||
binding.textSettingName.alpha = 1f
|
||||
binding.textSettingDescription.alpha = 1f
|
||||
binding.textSettingValue.alpha = 1f
|
||||
} else {
|
||||
binding.textSettingName.alpha = 0.5f
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
if (!setting.isEditable) {
|
||||
adapter.onClickDisabledSetting()
|
||||
if (!setting.isEditable || !setting.isEnabled) {
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
return
|
||||
}
|
||||
adapter.onStringInputClick((setting as StringInputSetting), bindingAdapterPosition)
|
||||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -33,26 +33,26 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
|
|||
adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked)
|
||||
}
|
||||
|
||||
binding.switchWidget.isEnabled = setting.isEditable
|
||||
binding.switchWidget.isEnabled = setting.isActive
|
||||
|
||||
val textAlpha = if (setting.isEditable) 1f else 0.5f
|
||||
val textAlpha = if (setting.isActive) 1f else 0.5f
|
||||
binding.textSettingName.alpha = textAlpha
|
||||
binding.textSettingDescription.alpha = textAlpha
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
binding.switchWidget.toggle()
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
if (setting.isActive) {
|
||||
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting()
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -141,7 +141,7 @@ class CopyDirProgressDialog : DialogFragment() {
|
|||
|
||||
override fun onComplete() {
|
||||
CitraDirectoryHelper.initializeCitraDirectory(path)
|
||||
callback?.onStepCompleted()
|
||||
callback?.onStepCompleted(0, false)
|
||||
viewModel.setCopyComplete(true)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.BatteryManager
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
@ -16,16 +20,19 @@ import android.os.SystemClock
|
|||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.Choreographer
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
|
@ -43,6 +50,7 @@ import androidx.navigation.fragment.navArgs
|
|||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.slider.Slider
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
|
@ -56,6 +64,7 @@ import org.citra.citra_emu.databinding.FragmentEmulationBinding
|
|||
import org.citra.citra_emu.display.PortraitScreenLayout
|
||||
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||
import org.citra.citra_emu.display.ScreenLayout
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
import org.citra.citra_emu.features.settings.model.SettingsViewModel
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||
|
@ -174,8 +183,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
binding.surfaceInputOverlay.setIsInEditMode(false)
|
||||
}
|
||||
|
||||
// Show/hide the "Show FPS" overlay
|
||||
updateShowFpsOverlay()
|
||||
// Show/hide the "Stats" overlay
|
||||
updateShowPerformanceOverlay()
|
||||
|
||||
val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int
|
||||
updateStatsPosition(position)
|
||||
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
binding.drawerLayout.addDrawerListener(object : DrawerListener {
|
||||
|
@ -232,8 +244,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
)
|
||||
}
|
||||
|
||||
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
|
||||
game.title
|
||||
binding.inGameMenu.getHeaderView(0).apply {
|
||||
val titleView = findViewById<TextView>(R.id.text_game_title)
|
||||
val iconView = findViewById<ImageView>(R.id.game_icon)
|
||||
|
||||
titleView.text = game.title
|
||||
|
||||
GameIconUtils.loadGameIcon(requireActivity(), game, iconView)
|
||||
}
|
||||
|
||||
binding.inGameMenu.setNavigationItemSelectedListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_emulation_pause -> {
|
||||
|
@ -287,6 +306,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
true
|
||||
}
|
||||
|
||||
R.id.menu_rotate_upright -> {
|
||||
screenAdjustmentUtil.toggleScreenUpright()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_lock_drawer -> {
|
||||
when (EmulationMenuSettings.drawerLockMode) {
|
||||
DrawerLayout.LOCK_MODE_UNLOCKED -> {
|
||||
|
@ -447,6 +471,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
Choreographer.getInstance().postFrameCallback(this)
|
||||
if (NativeLibrary.isRunning()) {
|
||||
NativeLibrary.unPauseEmulation()
|
||||
|
||||
// If the overlay is enabled, we need to update the position if changed
|
||||
val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int
|
||||
updateStatsPosition(position)
|
||||
|
||||
binding.inGameMenu.menu.findItem(R.id.menu_emulation_pause)?.let { menuItem ->
|
||||
menuItem.title = resources.getString(R.string.pause_emulation)
|
||||
menuItem.icon = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
R.drawable.ic_pause,
|
||||
requireContext().theme
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -533,7 +570,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
val slot = i
|
||||
var enableClick = isSaving
|
||||
val text = if (slot == NativeLibrary.QUICKSAVE_SLOT) {
|
||||
enableClick = false
|
||||
getString(R.string.emulation_quicksave_slot)
|
||||
} else {
|
||||
getString(R.string.emulation_empty_state_slot, slot)
|
||||
|
@ -542,11 +578,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
add(text).setEnabled(enableClick).setOnMenuItemClickListener {
|
||||
if(isSaving) {
|
||||
NativeLibrary.saveState(slot)
|
||||
Toast.makeText(context,
|
||||
getString(R.string.saving),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
NativeLibrary.loadState(slot)
|
||||
binding.drawerLayout.close()
|
||||
Toast.makeText(context,
|
||||
getString(R.string.quickload_loading),
|
||||
getString(R.string.loading),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
true
|
||||
|
@ -557,8 +596,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
savestates?.forEach {
|
||||
var enableClick = true
|
||||
val text = if(it.slot == NativeLibrary.QUICKSAVE_SLOT) {
|
||||
// do not allow saving in quicksave slot
|
||||
enableClick = !isSaving
|
||||
getString(R.string.emulation_occupied_quicksave_slot, it.time)
|
||||
} else{
|
||||
getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
|
||||
|
@ -624,7 +661,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
|
||||
popupMenu.menu.apply {
|
||||
findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
|
||||
findItem(R.id.menu_show_fps).isChecked = EmulationMenuSettings.showFps
|
||||
findItem(R.id.menu_performance_overlay_show).isChecked =
|
||||
EmulationMenuSettings.showPerformanceOverlay
|
||||
findItem(R.id.menu_haptic_feedback).isChecked = EmulationMenuSettings.hapticFeedback
|
||||
findItem(R.id.menu_emulation_joystick_rel_center).isChecked =
|
||||
EmulationMenuSettings.joystickRelCenter
|
||||
|
@ -640,15 +678,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
true
|
||||
}
|
||||
|
||||
R.id.menu_show_fps -> {
|
||||
EmulationMenuSettings.showFps = !EmulationMenuSettings.showFps
|
||||
updateShowFpsOverlay()
|
||||
R.id.menu_performance_overlay_show -> {
|
||||
EmulationMenuSettings.showPerformanceOverlay = !EmulationMenuSettings.showPerformanceOverlay
|
||||
updateShowPerformanceOverlay()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_haptic_feedback -> {
|
||||
EmulationMenuSettings.hapticFeedback = !EmulationMenuSettings.hapticFeedback
|
||||
updateShowFpsOverlay()
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -886,10 +923,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
val layoutOptionMenuItem = when (IntSetting.PORTRAIT_SCREEN_LAYOUT.int) {
|
||||
PortraitScreenLayout.TOP_FULL_WIDTH.int ->
|
||||
R.id.menu_portrait_layout_top_full
|
||||
|
||||
PortraitScreenLayout.ORIGINAL.int ->
|
||||
R.id.menu_portrait_layout_original
|
||||
PortraitScreenLayout.CUSTOM_PORTRAIT_LAYOUT.int ->
|
||||
R.id.menu_portrait_layout_custom
|
||||
|
||||
else ->
|
||||
R.id.menu_portrait_layout_top_full
|
||||
|
||||
|
@ -904,6 +941,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
true
|
||||
}
|
||||
|
||||
R.id.menu_portrait_layout_original -> {
|
||||
screenAdjustmentUtil.changePortraitOrientation(PortraitScreenLayout.ORIGINAL.int)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_portrait_layout_custom -> {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
|
@ -933,12 +975,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
|
||||
private fun showToggleControlsDialog() {
|
||||
val editor = preferences.edit()
|
||||
val enabledButtons = BooleanArray(15)
|
||||
val enabledButtons = BooleanArray(16)
|
||||
enabledButtons.forEachIndexed { i: Int, _: Boolean ->
|
||||
// Buttons that are disabled by default
|
||||
var defaultValue = true
|
||||
when (i) {
|
||||
6, 7, 12, 13, 14 -> defaultValue = false
|
||||
6, 7, 12, 13, 14, 15 -> defaultValue = false
|
||||
}
|
||||
enabledButtons[i] = preferences.getBoolean("buttonToggle$i", defaultValue)
|
||||
}
|
||||
|
@ -1116,10 +1158,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
.apply()
|
||||
|
||||
val editor = preferences.edit()
|
||||
for (i in 0 until 15) {
|
||||
for (i in 0 until 16) {
|
||||
var defaultValue = true
|
||||
when (i) {
|
||||
6, 7, 12, 13, 14 -> defaultValue = false
|
||||
6, 7, 12, 13, 14, 15 -> defaultValue = false
|
||||
}
|
||||
editor.putBoolean("buttonToggle$i", defaultValue)
|
||||
}
|
||||
|
@ -1128,34 +1170,150 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
binding.surfaceInputOverlay.resetButtonPlacement()
|
||||
}
|
||||
|
||||
fun updateShowFpsOverlay() {
|
||||
if (EmulationMenuSettings.showFps) {
|
||||
fun updateShowPerformanceOverlay() {
|
||||
if (perfStatsUpdater != null) {
|
||||
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
|
||||
}
|
||||
|
||||
if (EmulationMenuSettings.showPerformanceOverlay) {
|
||||
val SYSTEM_FPS = 0
|
||||
val FPS = 1
|
||||
val FRAMETIME = 2
|
||||
val SPEED = 3
|
||||
val SPEED = 2
|
||||
val FRAMETIME = 3
|
||||
val TIME_SVC = 4
|
||||
val TIME_IPC = 5
|
||||
val TIME_GPU = 6
|
||||
val TIME_SWAP = 7
|
||||
val TIME_REM = 8
|
||||
perfStatsUpdater = Runnable {
|
||||
val sb = StringBuilder()
|
||||
val perfStats = NativeLibrary.getPerfStats()
|
||||
val dividerString = "\u00A0\u2502 "
|
||||
if (perfStats[FPS] > 0) {
|
||||
binding.showFpsText.text = String.format(
|
||||
"FPS: %d Speed: %d%% FT: %.2fms",
|
||||
(perfStats[FPS] + 0.5).toInt(),
|
||||
(perfStats[SPEED] * 100.0 + 0.5).toInt(),
|
||||
(perfStats[FRAMETIME] * 1000.0f).toFloat()
|
||||
)
|
||||
if (BooleanSetting.OVERLAY_SHOW_FPS.boolean) {
|
||||
sb.append(String.format("FPS:\u00A0%d", (perfStats[FPS] + 0.5).toInt()))
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_FRAMETIME.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
sb.append(
|
||||
String.format(
|
||||
"Frame:\u00A0%.1fms (GPU: [CMD:\u00A0%.1fms SWP:\u00A0%.1fms] IPC:\u00A0%.1fms SVC:\u00A0%.1fms Rem:\u00A0%.1fms)",
|
||||
(perfStats[FRAMETIME] * 1000.0f).toFloat(),
|
||||
(perfStats[TIME_GPU] * 1000.0f).toFloat(),
|
||||
(perfStats[TIME_SWAP] * 1000.0f).toFloat(),
|
||||
(perfStats[TIME_IPC] * 1000.0f).toFloat(),
|
||||
(perfStats[TIME_SVC] * 1000.0f).toFloat(),
|
||||
(perfStats[TIME_REM] * 1000.0f).toFloat(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_SPEED.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
sb.append(
|
||||
String.format(
|
||||
"Speed:\u00A0%d%%",
|
||||
(perfStats[SPEED] * 100.0 + 0.5).toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_APP_RAM_USAGE.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
val appRamUsage =
|
||||
File("/proc/self/statm").readLines()[0].split(' ')[1].toLong() * 4096 / 1000000
|
||||
sb.append("Process\u00A0RAM:\u00A0$appRamUsage\u00A0MB")
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_AVAILABLE_RAM.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
context?.let { ctx ->
|
||||
val activityManager =
|
||||
ctx.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val memInfo = ActivityManager.MemoryInfo()
|
||||
activityManager.getMemoryInfo(memInfo)
|
||||
val megabyteBytes = 1048576L
|
||||
val availableRam = memInfo.availMem / megabyteBytes
|
||||
sb.append("Available\u00A0RAM:\u00A0$availableRam\u00A0MB")
|
||||
}
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_BATTERY_TEMP.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
val batteryTemp = getBatteryTemperature()
|
||||
val tempF = celsiusToFahrenheit(batteryTemp)
|
||||
sb.append(String.format("%.1f°C/%.1f°F", batteryTemp, tempF))
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_BACKGROUND.boolean) {
|
||||
binding.performanceOverlayShowText.setBackgroundResource(R.color.citra_transparent_black)
|
||||
} else {
|
||||
binding.performanceOverlayShowText.setBackgroundResource(0)
|
||||
}
|
||||
|
||||
binding.performanceOverlayShowText.text = sb.toString()
|
||||
}
|
||||
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 3000)
|
||||
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 1000)
|
||||
}
|
||||
perfStatsUpdateHandler.post(perfStatsUpdater!!)
|
||||
binding.showFpsText.visibility = View.VISIBLE
|
||||
binding.performanceOverlayShowText.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (perfStatsUpdater != null) {
|
||||
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
|
||||
}
|
||||
binding.showFpsText.visibility = View.GONE
|
||||
binding.performanceOverlayShowText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatsPosition(position: Int) {
|
||||
val params = binding.performanceOverlayShowText.layoutParams as CoordinatorLayout.LayoutParams
|
||||
val padding = (20 * resources.displayMetrics.density).toInt() // 20dp
|
||||
params.setMargins(padding, 0, padding, 0)
|
||||
|
||||
when (position) {
|
||||
0 -> {
|
||||
params.gravity = (Gravity.TOP or Gravity.START)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
params.gravity = (Gravity.TOP or Gravity.CENTER_HORIZONTAL)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
params.gravity = (Gravity.TOP or Gravity.END)
|
||||
}
|
||||
|
||||
3 -> {
|
||||
params.gravity = (Gravity.BOTTOM or Gravity.START)
|
||||
}
|
||||
|
||||
4 -> {
|
||||
params.gravity = (Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
|
||||
}
|
||||
|
||||
5 -> {
|
||||
params.gravity = (Gravity.BOTTOM or Gravity.END)
|
||||
}
|
||||
}
|
||||
|
||||
binding.performanceOverlayShowText.layoutParams = params
|
||||
}
|
||||
|
||||
private fun getBatteryTemperature(): Float {
|
||||
try {
|
||||
val batteryIntent = requireContext().registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
// Temperature in tenths of a degree Celsius
|
||||
val temperature = batteryIntent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0
|
||||
// Convert to degrees Celsius
|
||||
return temperature / 10.0f
|
||||
} catch (e: Exception) {
|
||||
return 0.0f
|
||||
}
|
||||
}
|
||||
|
||||
private fun celsiusToFahrenheit(celsius: Float): Float {
|
||||
return (celsius * 9 / 5) + 32
|
||||
}
|
||||
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
// We purposely don't do anything here.
|
||||
// All work is done in surfaceChanged, which we are guaranteed to get even for surface creation.
|
||||
|
@ -1190,23 +1348,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
|
||||
v.setPadding(left, cutInsets.top, right, 0)
|
||||
|
||||
// Ensure FPS text doesn't get cut off by rounded display corners
|
||||
val sidePadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
|
||||
if (cutInsets.left == 0) {
|
||||
binding.showFpsText.setPadding(
|
||||
sidePadding,
|
||||
cutInsets.top,
|
||||
cutInsets.right,
|
||||
cutInsets.bottom
|
||||
)
|
||||
} else {
|
||||
binding.showFpsText.setPadding(
|
||||
cutInsets.left,
|
||||
cutInsets.top,
|
||||
cutInsets.right,
|
||||
cutInsets.bottom
|
||||
)
|
||||
}
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package org.citra.citra_emu.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
|
@ -12,7 +13,10 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
|
@ -36,8 +40,6 @@ import org.citra.citra_emu.features.settings.model.Settings
|
|||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.text.HtmlCompat
|
||||
|
||||
class GamesFragment : Fragment() {
|
||||
private var _binding: FragmentGamesBinding? = null
|
||||
|
@ -46,6 +48,13 @@ class GamesFragment : Fragment() {
|
|||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private var show3DSFileWarning: Boolean = true
|
||||
private lateinit var gameAdapter: GameAdapter
|
||||
|
||||
private val openImageLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
gameAdapter.handleShortcutImageResult(uri)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -69,12 +78,18 @@ class GamesFragment : Fragment() {
|
|||
|
||||
val inflater = LayoutInflater.from(requireContext())
|
||||
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
)
|
||||
|
||||
binding.gridGames.apply {
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(),
|
||||
resources.getInteger(R.integer.game_grid_columns)
|
||||
)
|
||||
adapter = GameAdapter(requireActivity() as AppCompatActivity, inflater)
|
||||
adapter = this@GamesFragment.gameAdapter
|
||||
}
|
||||
|
||||
binding.swipeRefresh.apply {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -36,6 +36,8 @@ import org.citra.citra_emu.viewmodel.GamesViewModel
|
|||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import java.time.temporal.ChronoField
|
||||
import java.util.Locale
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
private var _binding: FragmentSearchBinding? = null
|
||||
|
@ -43,6 +45,13 @@ class SearchFragment : Fragment() {
|
|||
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private lateinit var gameAdapter: GameAdapter
|
||||
|
||||
private val openImageLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
gameAdapter.handleShortcutImageResult(uri)
|
||||
}
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
|
@ -73,12 +82,18 @@ class SearchFragment : Fragment() {
|
|||
|
||||
val inflater = LayoutInflater.from(requireContext())
|
||||
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
)
|
||||
|
||||
binding.gridGamesSearch.apply {
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(),
|
||||
resources.getInteger(R.integer.game_grid_columns)
|
||||
)
|
||||
adapter = GameAdapter(requireActivity() as AppCompatActivity, inflater)
|
||||
adapter = this@SearchFragment.gameAdapter
|
||||
}
|
||||
|
||||
binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
|
||||
|
|
|
@ -27,7 +27,7 @@ class SelectUserDirectoryDialogFragment : DialogFragment() {
|
|||
.setTitle(R.string.select_citra_user_folder)
|
||||
.setMessage(R.string.selecting_user_directory_without_write_permissions)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
mainActivity?.openCitraDirectory?.launch(null)
|
||||
mainActivity?.openCitraDirectoryLostPermission?.launch(null)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -35,9 +35,11 @@ import org.citra.citra_emu.R
|
|||
import org.citra.citra_emu.adapters.SetupAdapter
|
||||
import org.citra.citra_emu.databinding.FragmentSetupBinding
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.model.ButtonState
|
||||
import org.citra.citra_emu.model.PageButton
|
||||
import org.citra.citra_emu.model.PageState
|
||||
import org.citra.citra_emu.model.SetupCallback
|
||||
import org.citra.citra_emu.model.SetupPage
|
||||
import org.citra.citra_emu.model.StepState
|
||||
import org.citra.citra_emu.ui.main.MainActivity
|
||||
import org.citra.citra_emu.utils.CitraDirectoryHelper
|
||||
import org.citra.citra_emu.utils.GameHelper
|
||||
|
@ -113,151 +115,195 @@ class SetupFragment : Fragment() {
|
|||
0,
|
||||
true,
|
||||
R.string.get_started,
|
||||
{ pageForward() }
|
||||
)
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
add(
|
||||
SetupPage(
|
||||
R.drawable.ic_notification,
|
||||
R.string.notifications,
|
||||
R.string.notifications_description,
|
||||
0,
|
||||
false,
|
||||
R.string.give_permission,
|
||||
{
|
||||
notificationCallback = it
|
||||
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
},
|
||||
false,
|
||||
true,
|
||||
{
|
||||
if (NotificationManagerCompat.from(requireContext())
|
||||
.areNotificationsEnabled()
|
||||
) {
|
||||
StepState.STEP_COMPLETE
|
||||
} else {
|
||||
StepState.STEP_INCOMPLETE
|
||||
}
|
||||
},
|
||||
R.string.notification_warning,
|
||||
R.string.notification_warning_description,
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
add(
|
||||
SetupPage(
|
||||
R.drawable.ic_microphone,
|
||||
R.string.microphone_permission,
|
||||
R.string.microphone_permission_description,
|
||||
0,
|
||||
false,
|
||||
R.string.give_permission,
|
||||
{
|
||||
microphoneCallback = it
|
||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
},
|
||||
false,
|
||||
false,
|
||||
{
|
||||
if (
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
StepState.STEP_COMPLETE
|
||||
} else {
|
||||
StepState.STEP_INCOMPLETE
|
||||
}
|
||||
pageButtons = mutableListOf<PageButton>().apply {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_arrow_forward,
|
||||
R.string.get_started,
|
||||
0,
|
||||
buttonAction = {
|
||||
pageForward()
|
||||
},
|
||||
buttonState = {
|
||||
ButtonState.BUTTON_ACTION_UNDEFINED
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SetupPage(
|
||||
R.drawable.ic_camera,
|
||||
R.string.camera_permission,
|
||||
R.string.camera_permission_description,
|
||||
R.drawable.ic_permission,
|
||||
R.string.permissions,
|
||||
R.string.permissions_description,
|
||||
0,
|
||||
false,
|
||||
R.string.give_permission,
|
||||
{
|
||||
cameraCallback = it
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
},
|
||||
false,
|
||||
false,
|
||||
{
|
||||
if (
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
StepState.STEP_COMPLETE
|
||||
} else {
|
||||
StepState.STEP_INCOMPLETE
|
||||
0,
|
||||
pageButtons = mutableListOf<PageButton>().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_notification,
|
||||
R.string.notifications,
|
||||
R.string.notifications_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
},
|
||||
buttonState = {
|
||||
if (NotificationManagerCompat.from(requireContext())
|
||||
.areNotificationsEnabled()
|
||||
) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
},
|
||||
isUnskippable = false,
|
||||
hasWarning = true,
|
||||
R.string.notification_warning,
|
||||
R.string.notification_warning_description,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
SetupPage(
|
||||
R.drawable.ic_home,
|
||||
R.string.select_citra_user_folder,
|
||||
R.string.select_citra_user_folder_description,
|
||||
0,
|
||||
true,
|
||||
R.string.select,
|
||||
{
|
||||
userDirCallback = it
|
||||
openCitraDirectory.launch(null)
|
||||
},
|
||||
true,
|
||||
true,
|
||||
{
|
||||
if (PermissionsHandler.hasWriteAccess(requireContext())) {
|
||||
StepState.STEP_COMPLETE
|
||||
} else {
|
||||
StepState.STEP_INCOMPLETE
|
||||
}
|
||||
},
|
||||
R.string.cannot_skip,
|
||||
R.string.cannot_skip_directory_description,
|
||||
R.string.cannot_skip_directory_help
|
||||
)
|
||||
)
|
||||
add(
|
||||
SetupPage(
|
||||
R.drawable.ic_controller,
|
||||
R.string.games,
|
||||
R.string.games_description,
|
||||
0,
|
||||
true,
|
||||
R.string.select,
|
||||
{
|
||||
gamesDirCallback = it
|
||||
getGamesDirectory.launch(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_microphone,
|
||||
R.string.microphone_permission,
|
||||
R.string.microphone_permission_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
},
|
||||
buttonState = {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_camera,
|
||||
R.string.camera_permission,
|
||||
R.string.camera_permission_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
},
|
||||
buttonState = {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
false,
|
||||
true,
|
||||
{
|
||||
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
||||
StepState.STEP_COMPLETE
|
||||
} else {
|
||||
StepState.STEP_INCOMPLETE
|
||||
}
|
||||
},
|
||||
R.string.add_games_warning,
|
||||
R.string.add_games_warning_description,
|
||||
R.string.add_games_warning_help
|
||||
)
|
||||
) {
|
||||
if (
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) == PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED &&
|
||||
NotificationManagerCompat.from(requireContext())
|
||||
.areNotificationsEnabled()
|
||||
) {
|
||||
PageState.PAGE_STEPS_COMPLETE
|
||||
} else {
|
||||
PageState.PAGE_STEPS_INCOMPLETE
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
add(
|
||||
SetupPage(
|
||||
R.drawable.ic_folder,
|
||||
R.string.select_emulator_data_folders,
|
||||
R.string.select_emulator_data_folders_description,
|
||||
0,
|
||||
true,
|
||||
R.string.select,
|
||||
pageButtons = mutableListOf<PageButton>().apply {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_home,
|
||||
R.string.select_citra_user_folder,
|
||||
R.string.select_citra_user_folder_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
openCitraDirectory.launch(null)
|
||||
},
|
||||
buttonState = {
|
||||
if (PermissionsHandler.hasWriteAccess(requireContext())) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
},
|
||||
isUnskippable = true,
|
||||
hasWarning = false,
|
||||
R.string.cannot_skip,
|
||||
R.string.cannot_skip_directory_description,
|
||||
R.string.cannot_skip_directory_help
|
||||
|
||||
)
|
||||
)
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_controller,
|
||||
R.string.games,
|
||||
R.string.games_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
getGamesDirectory.launch(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
||||
)
|
||||
},
|
||||
buttonState = {
|
||||
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
},
|
||||
isUnskippable = false,
|
||||
hasWarning = true,
|
||||
R.string.add_games_warning,
|
||||
R.string.add_games_warning_description,
|
||||
)
|
||||
)
|
||||
},
|
||||
) {
|
||||
if (
|
||||
PermissionsHandler.hasWriteAccess(requireContext()) &&
|
||||
preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()
|
||||
) {
|
||||
PageState.PAGE_STEPS_COMPLETE
|
||||
|
||||
} else {
|
||||
PageState.PAGE_STEPS_INCOMPLETE
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
add(
|
||||
SetupPage(
|
||||
R.drawable.ic_check,
|
||||
|
@ -266,7 +312,21 @@ class SetupFragment : Fragment() {
|
|||
R.drawable.ic_arrow_forward,
|
||||
false,
|
||||
R.string.text_continue,
|
||||
{ finishSetup() }
|
||||
pageButtons = mutableListOf<PageButton>().apply {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_arrow_forward,
|
||||
R.string.text_continue,
|
||||
0,
|
||||
buttonAction = {
|
||||
finishSetup()
|
||||
},
|
||||
buttonState = {
|
||||
ButtonState.BUTTON_ACTION_UNDEFINED
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -303,35 +363,47 @@ class SetupFragment : Fragment() {
|
|||
val index = binding.viewPager2.currentItem
|
||||
val currentPage = pages[index]
|
||||
|
||||
// Checks if the user has completed the task on the current page
|
||||
if (currentPage.hasWarning || currentPage.isUnskippable) {
|
||||
val stepState = currentPage.stepCompleted.invoke()
|
||||
if (stepState == StepState.STEP_COMPLETE ||
|
||||
stepState == StepState.STEP_UNDEFINED
|
||||
) {
|
||||
pageForward()
|
||||
return@setOnClickListener
|
||||
}
|
||||
// This allows multiple sets of warning messages to be displayed on the same dialog if necessary
|
||||
val warningMessages =
|
||||
mutableListOf<Triple<Int, Int, Int>>() // title, description, helpLink
|
||||
|
||||
if (currentPage.isUnskippable) {
|
||||
MessageDialogFragment.newInstance(
|
||||
currentPage.warningTitleId,
|
||||
currentPage.warningDescriptionId,
|
||||
currentPage.warningHelpLinkId
|
||||
).show(childFragmentManager, MessageDialogFragment.TAG)
|
||||
return@setOnClickListener
|
||||
}
|
||||
currentPage.pageButtons?.forEach { button ->
|
||||
if (button.hasWarning || button.isUnskippable) {
|
||||
val buttonState = button.buttonState()
|
||||
if (buttonState == ButtonState.BUTTON_ACTION_COMPLETE) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (!hasBeenWarned[index]) {
|
||||
SetupWarningDialogFragment.newInstance(
|
||||
currentPage.warningTitleId,
|
||||
currentPage.warningDescriptionId,
|
||||
currentPage.warningHelpLinkId,
|
||||
index
|
||||
).show(childFragmentManager, SetupWarningDialogFragment.TAG)
|
||||
return@setOnClickListener
|
||||
if (button.isUnskippable) {
|
||||
MessageDialogFragment.newInstance(
|
||||
button.warningTitleId,
|
||||
button.warningDescriptionId,
|
||||
button.warningHelpLinkId
|
||||
).show(childFragmentManager, MessageDialogFragment.TAG)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (!hasBeenWarned[index]) {
|
||||
warningMessages.add(
|
||||
Triple(
|
||||
button.warningTitleId,
|
||||
button.warningDescriptionId,
|
||||
button.warningHelpLinkId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (warningMessages.isNotEmpty()) {
|
||||
SetupWarningDialogFragment.newInstance(
|
||||
warningMessages.map { it.first }.toIntArray(),
|
||||
warningMessages.map { it.second }.toIntArray(),
|
||||
warningMessages.map { it.third }.toIntArray(),
|
||||
index
|
||||
).show(childFragmentManager, SetupWarningDialogFragment.TAG)
|
||||
return@setOnClickListener
|
||||
}
|
||||
pageForward()
|
||||
}
|
||||
binding.buttonBack.setOnClickListener { pageBackward() }
|
||||
|
@ -366,19 +438,24 @@ class SetupFragment : Fragment() {
|
|||
_binding = null
|
||||
}
|
||||
|
||||
private lateinit var notificationCallback: SetupCallback
|
||||
private lateinit var microphoneCallback: SetupCallback
|
||||
private lateinit var cameraCallback: SetupCallback
|
||||
private lateinit var pageButtonCallback: SetupCallback
|
||||
private val checkForButtonState: () -> Unit = {
|
||||
val page = pages[binding.viewPager2.currentItem]
|
||||
page.pageButtons?.forEach {
|
||||
if (it.buttonState() == ButtonState.BUTTON_ACTION_COMPLETE) {
|
||||
pageButtonCallback.onStepCompleted(it.titleId, pageFullyCompleted = false)
|
||||
}
|
||||
|
||||
if (page.pageSteps() == PageState.PAGE_STEPS_COMPLETE) {
|
||||
pageButtonCallback.onStepCompleted(0, pageFullyCompleted = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val permissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
val page = pages[binding.viewPager2.currentItem]
|
||||
when (page.titleId) {
|
||||
R.string.notifications -> notificationCallback.onStepCompleted()
|
||||
R.string.microphone_permission -> microphoneCallback.onStepCompleted()
|
||||
R.string.camera_permission -> cameraCallback.onStepCompleted()
|
||||
}
|
||||
checkForButtonState.invoke()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
|
@ -394,8 +471,6 @@ class SetupFragment : Fragment() {
|
|||
.show()
|
||||
}
|
||||
|
||||
private lateinit var userDirCallback: SetupCallback
|
||||
|
||||
private val openCitraDirectory = registerForActivityResult<Uri, Uri>(
|
||||
ActivityResultContracts.OpenDocumentTree()
|
||||
) { result: Uri? ->
|
||||
|
@ -403,11 +478,9 @@ class SetupFragment : Fragment() {
|
|||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
CitraDirectoryHelper(requireActivity()).showCitraDirectoryDialog(result, userDirCallback)
|
||||
CitraDirectoryHelper(requireActivity(), true).showCitraDirectoryDialog(result, pageButtonCallback, checkForButtonState)
|
||||
}
|
||||
|
||||
private lateinit var gamesDirCallback: SetupCallback
|
||||
|
||||
private val getGamesDirectory =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||
if (result == null) {
|
||||
|
@ -427,7 +500,7 @@ class SetupFragment : Fragment() {
|
|||
|
||||
homeViewModel.setGamesDir(requireActivity(), result.path!!)
|
||||
|
||||
gamesDirCallback.onStepCompleted()
|
||||
checkForButtonState.invoke()
|
||||
}
|
||||
|
||||
private fun finishSetup() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -14,18 +14,18 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import org.citra.citra_emu.R
|
||||
|
||||
class SetupWarningDialogFragment : DialogFragment() {
|
||||
private var titleId: Int = 0
|
||||
private var descriptionId: Int = 0
|
||||
private var helpLinkId: Int = 0
|
||||
private var titleIds: IntArray = intArrayOf()
|
||||
private var descriptionIds: IntArray = intArrayOf()
|
||||
private var helpLinkIds: IntArray = intArrayOf()
|
||||
private var page: Int = 0
|
||||
|
||||
private lateinit var setupFragment: SetupFragment
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
titleId = requireArguments().getInt(TITLE)
|
||||
descriptionId = requireArguments().getInt(DESCRIPTION)
|
||||
helpLinkId = requireArguments().getInt(HELP_LINK)
|
||||
titleIds = requireArguments().getIntArray(TITLES) ?: intArrayOf()
|
||||
descriptionIds = requireArguments().getIntArray(DESCRIPTIONS) ?: intArrayOf()
|
||||
helpLinkIds = requireArguments().getIntArray(HELP_LINKS) ?: intArrayOf()
|
||||
page = requireArguments().getInt(PAGE)
|
||||
|
||||
setupFragment = requireParentFragment() as SetupFragment
|
||||
|
@ -39,16 +39,23 @@ class SetupWarningDialogFragment : DialogFragment() {
|
|||
}
|
||||
.setNegativeButton(R.string.warning_cancel, null)
|
||||
|
||||
if (titleId != 0) {
|
||||
builder.setTitle(titleId)
|
||||
} else {
|
||||
builder.setTitle("")
|
||||
// Message builder to build multiple strings into one
|
||||
val messageBuilder = StringBuilder()
|
||||
for (i in titleIds.indices) {
|
||||
if (titleIds[i] != 0) {
|
||||
messageBuilder.append(getString(titleIds[i])).append("\n\n")
|
||||
}
|
||||
if (descriptionIds[i] != 0) {
|
||||
messageBuilder.append(getString(descriptionIds[i])).append("\n\n")
|
||||
}
|
||||
}
|
||||
if (descriptionId != 0) {
|
||||
builder.setMessage(descriptionId)
|
||||
}
|
||||
if (helpLinkId != 0) {
|
||||
|
||||
builder.setTitle("Warning")
|
||||
builder.setMessage(messageBuilder.toString().trim())
|
||||
|
||||
if (helpLinkIds.any { it != 0 }) {
|
||||
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
|
||||
val helpLinkId = helpLinkIds.first { it != 0 }
|
||||
val helpLink = resources.getString(helpLinkId)
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
|
||||
startActivity(intent)
|
||||
|
@ -61,23 +68,23 @@ class SetupWarningDialogFragment : DialogFragment() {
|
|||
companion object {
|
||||
const val TAG = "SetupWarningDialogFragment"
|
||||
|
||||
private const val TITLE = "Title"
|
||||
private const val DESCRIPTION = "Description"
|
||||
private const val HELP_LINK = "HelpLink"
|
||||
private const val TITLES = "Titles"
|
||||
private const val DESCRIPTIONS = "Descriptions"
|
||||
private const val HELP_LINKS = "HelpLinks"
|
||||
private const val PAGE = "Page"
|
||||
|
||||
fun newInstance(
|
||||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
helpLinkId: Int,
|
||||
titleIds: IntArray,
|
||||
descriptionIds: IntArray,
|
||||
helpLinkIds: IntArray,
|
||||
page: Int
|
||||
): SetupWarningDialogFragment {
|
||||
val dialog = SetupWarningDialogFragment()
|
||||
val bundle = Bundle()
|
||||
bundle.apply {
|
||||
putInt(TITLE, titleId)
|
||||
putInt(DESCRIPTION, descriptionId)
|
||||
putInt(HELP_LINK, helpLinkId)
|
||||
putIntArray(TITLES, titleIds)
|
||||
putIntArray(DESCRIPTIONS, descriptionIds)
|
||||
putIntArray(HELP_LINKS, helpLinkIds)
|
||||
putInt(PAGE, page)
|
||||
}
|
||||
dialog.arguments = bundle
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -11,21 +11,35 @@ data class SetupPage(
|
|||
val buttonIconId: Int,
|
||||
val leftAlignedIcon: Boolean,
|
||||
val buttonTextId: Int,
|
||||
val pageButtons: List<PageButton>? = null,
|
||||
val pageSteps: () -> PageState = { PageState.PAGE_STEPS_UNDEFINED },
|
||||
)
|
||||
|
||||
data class PageButton(
|
||||
val iconId: Int,
|
||||
val titleId: Int,
|
||||
val descriptionId: Int,
|
||||
val buttonAction: (callback: SetupCallback) -> Unit,
|
||||
val buttonState: () -> ButtonState = { ButtonState.BUTTON_ACTION_UNDEFINED },
|
||||
val isUnskippable: Boolean = false,
|
||||
val hasWarning: Boolean = false,
|
||||
val stepCompleted: () -> StepState = { StepState.STEP_UNDEFINED },
|
||||
val warningTitleId: Int = 0,
|
||||
val warningDescriptionId: Int = 0,
|
||||
val warningHelpLinkId: Int = 0
|
||||
)
|
||||
|
||||
interface SetupCallback {
|
||||
fun onStepCompleted()
|
||||
fun onStepCompleted(pageButtonId : Int, pageFullyCompleted: Boolean)
|
||||
}
|
||||
|
||||
enum class StepState {
|
||||
STEP_COMPLETE,
|
||||
STEP_INCOMPLETE,
|
||||
STEP_UNDEFINED
|
||||
enum class PageState {
|
||||
PAGE_STEPS_COMPLETE,
|
||||
PAGE_STEPS_INCOMPLETE,
|
||||
PAGE_STEPS_UNDEFINED
|
||||
}
|
||||
|
||||
enum class ButtonState {
|
||||
BUTTON_ACTION_COMPLETE,
|
||||
BUTTON_ACTION_INCOMPLETE,
|
||||
BUTTON_ACTION_UNDEFINED
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -25,6 +25,7 @@ import org.citra.citra_emu.CitraApplication
|
|||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
import org.citra.citra_emu.utils.TurboHelper
|
||||
import java.lang.NullPointerException
|
||||
import kotlin.math.min
|
||||
|
||||
|
@ -44,6 +45,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||
private var buttonBeingConfigured: InputOverlayDrawableButton? = null
|
||||
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
|
||||
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
|
||||
private val settingsViewModel = NativeLibrary.sEmulationActivity.get()!!.settingsViewModel
|
||||
|
||||
// Stores the ID of the pointer that interacted with the 3DS touchscreen.
|
||||
private var touchscreenPointerId = -1
|
||||
|
@ -104,6 +106,10 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||
swapScreen()
|
||||
}
|
||||
|
||||
if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) {
|
||||
TurboHelper.toggleTurbo(true)
|
||||
}
|
||||
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.id, button.status)
|
||||
shouldUpdateView = true
|
||||
}
|
||||
|
@ -468,6 +474,18 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("buttonToggle15", false)) {
|
||||
overlayButtons.add(
|
||||
initializeOverlayButton(
|
||||
context,
|
||||
R.drawable.button_turbo,
|
||||
R.drawable.button_turbo_pressed,
|
||||
NativeLibrary.ButtonType.BUTTON_TURBO,
|
||||
orientation
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshControls() {
|
||||
|
@ -673,6 +691,14 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||
NativeLibrary.ButtonType.BUTTON_SWAP.toString() + "-Y",
|
||||
resources.getInteger(R.integer.N3DS_BUTTON_SWAP_Y).toFloat() / 1000 * maxY
|
||||
)
|
||||
.putFloat(
|
||||
NativeLibrary.ButtonType.BUTTON_TURBO.toString() + "-X",
|
||||
resources.getInteger(R.integer.N3DS_BUTTON_TURBO_X).toFloat() / 1000 * maxX
|
||||
)
|
||||
.putFloat(
|
||||
NativeLibrary.ButtonType.BUTTON_TURBO.toString() + "-Y",
|
||||
resources.getInteger(R.integer.N3DS_BUTTON_TURBO_Y).toFloat() / 1000 * maxY
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
|
@ -816,6 +842,14 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||
NativeLibrary.ButtonType.BUTTON_SWAP.toString() + portrait + "-Y",
|
||||
resources.getInteger(R.integer.N3DS_BUTTON_SWAP_PORTRAIT_Y).toFloat() / 1000 * maxY
|
||||
)
|
||||
.putFloat(
|
||||
NativeLibrary.ButtonType.BUTTON_TURBO.toString() + portrait + "-X",
|
||||
resources.getInteger(R.integer.N3DS_BUTTON_TURBO_PORTRAIT_X).toFloat() / 1000 * maxX
|
||||
)
|
||||
.putFloat(
|
||||
NativeLibrary.ButtonType.BUTTON_TURBO.toString() + portrait + "-Y",
|
||||
resources.getInteger(R.integer.N3DS_BUTTON_TURBO_PORTRAIT_Y).toFloat() / 1000 * maxY
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
|
@ -928,6 +962,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||
NativeLibrary.ButtonType.BUTTON_START,
|
||||
NativeLibrary.ButtonType.BUTTON_SELECT,
|
||||
NativeLibrary.ButtonType.BUTTON_SWAP -> 0.08f
|
||||
NativeLibrary.ButtonType.BUTTON_TURBO -> 0.10f
|
||||
|
||||
NativeLibrary.ButtonType.TRIGGER_L,
|
||||
NativeLibrary.ButtonType.TRIGGER_R,
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.view.ViewGroup.MarginLayoutParams
|
|||
import android.view.WindowManager
|
||||
import android.view.animation.PathInterpolator
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -307,16 +308,22 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
windowInsets
|
||||
}
|
||||
|
||||
val openCitraDirectory = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree()
|
||||
) { result: Uri? ->
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
private fun createOpenCitraDirectoryLauncher(
|
||||
permissionsLost: Boolean
|
||||
): ActivityResultLauncher<Uri?> {
|
||||
return registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result: Uri? ->
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
CitraDirectoryHelper(this@MainActivity).showCitraDirectoryDialog(result)
|
||||
CitraDirectoryHelper(this@MainActivity, permissionsLost)
|
||||
.showCitraDirectoryDialog(result, buttonState = {})
|
||||
}
|
||||
}
|
||||
|
||||
val openCitraDirectory = createOpenCitraDirectoryLauncher(permissionsLost = false)
|
||||
val openCitraDirectoryLostPermission = createOpenCitraDirectoryLauncher(permissionsLost = true)
|
||||
|
||||
val ciaFileInstaller = registerForActivityResult(
|
||||
OpenFileResultContract()
|
||||
) { result: Intent? ->
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -16,15 +16,15 @@ import org.citra.citra_emu.viewmodel.HomeViewModel
|
|||
/**
|
||||
* Citra directory initialization ui flow controller.
|
||||
*/
|
||||
class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity) {
|
||||
fun showCitraDirectoryDialog(result: Uri, callback: SetupCallback? = null) {
|
||||
class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity, private val lostPermission: Boolean) {
|
||||
fun showCitraDirectoryDialog(result: Uri, callback: SetupCallback? = null, buttonState: () -> Unit) {
|
||||
val citraDirectoryDialog = CitraDirectoryDialogFragment.newInstance(
|
||||
fragmentActivity,
|
||||
result.toString(),
|
||||
CitraDirectoryDialogFragment.Listener { moveData: Boolean, path: Uri ->
|
||||
val previous = PermissionsHandler.citraDirectory
|
||||
// Do noting if user select the previous path.
|
||||
if (path == previous) {
|
||||
if (path == previous && !lostPermission) {
|
||||
return@Listener
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity) {
|
|||
)
|
||||
if (!moveData || previous.toString().isEmpty()) {
|
||||
initializeCitraDirectory(path)
|
||||
callback?.onStepCompleted()
|
||||
buttonState()
|
||||
val viewModel = ViewModelProvider(fragmentActivity)[HomeViewModel::class.java]
|
||||
viewModel.setUserDir(fragmentActivity, path.path!!)
|
||||
viewModel.setPickingUserDir(false)
|
||||
|
|
|
@ -106,6 +106,40 @@ class DocumentsTree {
|
|||
return node.uri ?: return Uri.EMPTY
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun folderUriHelper(path: String, createIfNotExists: Boolean = false): Uri? {
|
||||
root ?: return null
|
||||
val components = path.split(DELIMITER).filter { it.isNotEmpty() }
|
||||
var current = root
|
||||
|
||||
for (component in components) {
|
||||
if (!current!!.loaded) {
|
||||
structTree(current)
|
||||
}
|
||||
|
||||
var child = current.findChild(component)
|
||||
|
||||
// Create directory if it doesn't exist and creation is enabled
|
||||
if (child == null && createIfNotExists) {
|
||||
try {
|
||||
val createdDir = FileUtil.createDir(current.uri.toString(), component) ?: return null
|
||||
child = DocumentsNode(createdDir, true).apply {
|
||||
parent = current
|
||||
}
|
||||
current.addChild(child)
|
||||
} catch (e: Exception) {
|
||||
error("[DocumentsTree]: Cannot create directory, error: " + e.message)
|
||||
return null
|
||||
}
|
||||
} else if (child == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
current = child
|
||||
}
|
||||
return current?.uri
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun isDirectory(filepath: String): Boolean {
|
||||
val node = resolvePath(filepath) ?: return false
|
||||
|
|
|
@ -27,11 +27,11 @@ object EmulationMenuSettings {
|
|||
.apply()
|
||||
}
|
||||
|
||||
var showFps: Boolean
|
||||
get() = preferences.getBoolean("EmulationMenuSettings_ShowFps", false)
|
||||
var showPerformanceOverlay: Boolean
|
||||
get() = preferences.getBoolean("EmulationMenuSettings_showPerformanceOverlay", false)
|
||||
set(value) {
|
||||
preferences.edit()
|
||||
.putBoolean("EmulationMenuSettings_ShowFps", value)
|
||||
.putBoolean("EmulationMenuSettings_showPerformanceOverlay", value)
|
||||
.apply()
|
||||
}
|
||||
var hapticFeedback: Boolean
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
@ -34,7 +34,8 @@ object PermissionsHandler {
|
|||
|
||||
context.contentResolver.releasePersistableUriPermission(uri, takeFlags)
|
||||
} catch (e: Exception) {
|
||||
Log.error("[PermissionsHandler]: Cannot check citra data directory permission, error: " + e.message)
|
||||
// Do not use native library logging, as the native library may not be loaded yet
|
||||
android.util.Log.e("PermissionsHandler", "Cannot check citra data directory permission, error: ${e.message}")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.utils
|
||||
|
||||
import android.widget.Toast
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
|
||||
object TurboHelper {
|
||||
private var turboSpeedEnabled = false
|
||||
|
||||
fun isTurboSpeedEnabled(): Boolean {
|
||||
return turboSpeedEnabled
|
||||
}
|
||||
|
||||
fun reloadTurbo(showToast: Boolean) {
|
||||
val context = CitraApplication.appContext
|
||||
val toastMessage: String
|
||||
|
||||
if (turboSpeedEnabled) {
|
||||
NativeLibrary.setTemporaryFrameLimit(IntSetting.TURBO_LIMIT.int.toDouble())
|
||||
toastMessage = context.getString(R.string.turbo_enabled_toast)
|
||||
} else {
|
||||
NativeLibrary.disableTemporaryFrameLimit()
|
||||
toastMessage = context.getString(R.string.turbo_disabled_toast)
|
||||
}
|
||||
|
||||
if (showToast) {
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTurboEnabled(state: Boolean, showToast: Boolean) {
|
||||
turboSpeedEnabled = state
|
||||
reloadTurbo(showToast)
|
||||
}
|
||||
|
||||
fun toggleTurbo(showToast: Boolean) {
|
||||
setTurboEnabled(!TurboHelper.isTurboSpeedEnabled(), showToast)
|
||||
}
|
||||
}
|
|
@ -142,6 +142,7 @@ void Config::ReadValues() {
|
|||
ReadSetting("Renderer", Settings::values.async_presentation);
|
||||
ReadSetting("Renderer", Settings::values.async_shader_compilation);
|
||||
ReadSetting("Renderer", Settings::values.spirv_shader_gen);
|
||||
ReadSetting("Renderer", Settings::values.disable_spirv_optimizer);
|
||||
ReadSetting("Renderer", Settings::values.use_hw_shader);
|
||||
ReadSetting("Renderer", Settings::values.use_shader_jit);
|
||||
ReadSetting("Renderer", Settings::values.resolution_factor);
|
||||
|
@ -149,8 +150,8 @@ void Config::ReadValues() {
|
|||
ReadSetting("Renderer", Settings::values.use_vsync_new);
|
||||
ReadSetting("Renderer", Settings::values.texture_filter);
|
||||
ReadSetting("Renderer", Settings::values.texture_sampling);
|
||||
|
||||
// Work around to map Android setting for enabling the frame limiter to the format Citra expects
|
||||
ReadSetting("Renderer", Settings::values.turbo_limit);
|
||||
// Workaround to map Android setting for enabling the frame limiter to the format Citra expects
|
||||
if (sdl2_config->GetBoolean("Renderer", "use_frame_limit", true)) {
|
||||
ReadSetting("Renderer", Settings::values.frame_limit);
|
||||
} else {
|
||||
|
@ -183,11 +184,13 @@ void Config::ReadValues() {
|
|||
layoutInt = static_cast<int>(Settings::LayoutOption::LargeScreen);
|
||||
}
|
||||
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layoutInt);
|
||||
Settings::values.screen_gap = static_cast<int>(sdl2_config->GetReal("Layout", "screen_gap", 0));
|
||||
Settings::values.large_screen_proportion =
|
||||
static_cast<float>(sdl2_config->GetReal("Layout", "large_screen_proportion", 2.25));
|
||||
Settings::values.small_screen_position = static_cast<Settings::SmallScreenPosition>(
|
||||
sdl2_config->GetInteger("Layout", "small_screen_position",
|
||||
static_cast<int>(Settings::SmallScreenPosition::TopRight)));
|
||||
ReadSetting("Layout", Settings::values.screen_gap);
|
||||
ReadSetting("Layout", Settings::values.custom_top_x);
|
||||
ReadSetting("Layout", Settings::values.custom_top_y);
|
||||
ReadSetting("Layout", Settings::values.custom_top_width);
|
||||
|
@ -195,10 +198,12 @@ void Config::ReadValues() {
|
|||
ReadSetting("Layout", Settings::values.custom_bottom_x);
|
||||
ReadSetting("Layout", Settings::values.custom_bottom_y);
|
||||
ReadSetting("Layout", Settings::values.custom_bottom_width);
|
||||
ReadSetting("Layout", Settings::values.aspect_ratio);
|
||||
ReadSetting("Layout", Settings::values.custom_bottom_height);
|
||||
ReadSetting("Layout", Settings::values.cardboard_screen_size);
|
||||
ReadSetting("Layout", Settings::values.cardboard_x_shift);
|
||||
ReadSetting("Layout", Settings::values.cardboard_y_shift);
|
||||
ReadSetting("Layout", Settings::values.upright_screen);
|
||||
|
||||
Settings::values.portrait_layout_option =
|
||||
static_cast<Settings::PortraitLayoutOption>(sdl2_config->GetInteger(
|
||||
|
@ -290,6 +295,7 @@ void Config::ReadValues() {
|
|||
ReadSetting("Debugging", Settings::values.use_gdbstub);
|
||||
ReadSetting("Debugging", Settings::values.gdbstub_port);
|
||||
ReadSetting("Debugging", Settings::values.instant_debug_log);
|
||||
ReadSetting("Debugging", Settings::values.enable_rpc_server);
|
||||
|
||||
for (const auto& service_module : Service::service_module_map) {
|
||||
bool use_lle = sdl2_config->GetBoolean("Debugging", "LLE\\" + service_module.name, false);
|
||||
|
|
|
@ -113,6 +113,10 @@ async_shader_compilation =
|
|||
# 0: GLSL, 1: SPIR-V (default)
|
||||
spirv_shader_gen =
|
||||
|
||||
# Whether to disable the SPIRV optimizer. Disabling it reduces stutter, but may slightly worsen performance
|
||||
# 0: Enabled, 1: Disabled (default)
|
||||
disable_spirv_optimizer =
|
||||
|
||||
# Whether to use hardware shaders to emulate 3DS shaders
|
||||
# 0: Software, 1 (default): Hardware
|
||||
use_hw_shader =
|
||||
|
@ -156,6 +160,10 @@ use_frame_limit =
|
|||
# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default)
|
||||
frame_limit =
|
||||
|
||||
# Alternative frame limit which can be triggered by the user
|
||||
# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default)
|
||||
turbo_limit =
|
||||
|
||||
# The clear color for the renderer. What shows up on the sides of the bottom screen.
|
||||
# Must be in range of 0.0-1.0. Defaults to 0.0 for all.
|
||||
bg_red =
|
||||
|
@ -167,7 +175,7 @@ bg_green =
|
|||
render_3d =
|
||||
|
||||
# Change 3D Intensity
|
||||
# 0 - 100: Intensity. 0 (default)
|
||||
# 0 - 255: Intensity. 0 (default)
|
||||
factor_3d =
|
||||
|
||||
# The name of the post processing shader to apply.
|
||||
|
@ -202,6 +210,21 @@ disable_right_eye_render =
|
|||
# 5: Custom Layout
|
||||
layout_option =
|
||||
|
||||
# Position of the performance overlay
|
||||
# 0: Top Left
|
||||
# 1: Center Top
|
||||
# 2: Top Right
|
||||
# 3: Bottom Left
|
||||
# 4: Center Bottom
|
||||
# 5: Bottom Right
|
||||
performance_overlay_position =
|
||||
|
||||
# Screen Gap - adds a gap between screens in all two-screen modes
|
||||
# Measured in pixels relative to the 240px default height of the screens
|
||||
# Scales with the larger screen (so 24 is 10% of the larger screen height)
|
||||
# Default value is 0.0
|
||||
screen_gap =
|
||||
|
||||
# Large Screen Proportion - Relative size of large:small in large screen mode
|
||||
# Default value is 2.25
|
||||
large_screen_proportion =
|
||||
|
@ -256,6 +279,10 @@ custom_portrait_bottom_height =
|
|||
# 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent
|
||||
swap_screen =
|
||||
|
||||
# Expands the display area to include the cutout (or notch) area
|
||||
# 0 (default): Off, 1: On
|
||||
expand_to_cutout_area =
|
||||
|
||||
# Screen placement settings when using Cardboard VR (render3d = 4)
|
||||
# 30 - 100: Screen size as a percentage of the viewport. 85 (default)
|
||||
cardboard_screen_size =
|
||||
|
@ -422,6 +449,10 @@ gdbstub_port=24689
|
|||
# Immediately commits the debug log to file. Use this if Azahar crashes and the log output is being cut.
|
||||
instant_debug_log =
|
||||
|
||||
# Enable RPC server for scripting purposes. Allows accessing guest memory remotely.
|
||||
# 0 (default): Off, 1: On
|
||||
enable_rpc_server =
|
||||
|
||||
# Delay the start of apps when LLE modules are enabled
|
||||
# 0: Off, 1 (default): On
|
||||
delay_start_for_lle_modules =
|
||||
|
|
|
@ -215,7 +215,8 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
|
|||
LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
|
||||
|
||||
std::unique_ptr<Frontend::GraphicsContext> cpu_context;
|
||||
system.GPU().Renderer().Rasterizer()->LoadDiskResources(stop_run, &LoadDiskCacheProgress);
|
||||
system.GPU().Renderer().Rasterizer()->LoadDefaultDiskResources(stop_run,
|
||||
&LoadDiskCacheProgress);
|
||||
|
||||
LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
|
||||
|
||||
|
@ -643,16 +644,19 @@ void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNI
|
|||
jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_getPerfStats(JNIEnv* env,
|
||||
[[maybe_unused]] jobject obj) {
|
||||
auto& core = Core::System::GetInstance();
|
||||
jdoubleArray j_stats = env->NewDoubleArray(4);
|
||||
jdoubleArray j_stats = env->NewDoubleArray(9);
|
||||
|
||||
if (core.IsPoweredOn()) {
|
||||
auto results = core.GetAndResetPerfStats();
|
||||
|
||||
// Converting the structure into an array makes it easier to pass it to the frontend
|
||||
double stats[4] = {results.system_fps, results.game_fps, results.frametime,
|
||||
results.emulation_speed};
|
||||
double stats[9] = {results.system_fps, results.game_fps,
|
||||
results.emulation_speed, results.time_vblank_interval,
|
||||
results.time_hle_svc, results.time_hle_ipc,
|
||||
results.time_gpu, results.time_swap,
|
||||
results.time_remaining};
|
||||
|
||||
env->SetDoubleArrayRegion(j_stats, 0, 4, stats);
|
||||
env->SetDoubleArrayRegion(j_stats, 0, 9, stats);
|
||||
}
|
||||
|
||||
return j_stats;
|
||||
|
@ -781,4 +785,14 @@ void Java_org_citra_citra_1emu_NativeLibrary_unlinkConsole(JNIEnv* env, jobject
|
|||
HW::UniqueData::UnlinkConsole();
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_setTemporaryFrameLimit(JNIEnv* env, jobject obj,
|
||||
jdouble speed) {
|
||||
Settings::temporary_frame_limit = speed;
|
||||
Settings::is_temporary_frame_limit = true;
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_disableTemporaryFrameLimit(JNIEnv* env, jobject obj) {
|
||||
Settings::is_temporary_frame_limit = false;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
|
BIN
src/android/app/src/main/res/drawable-hdpi/button_turbo.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.6 KiB |
BIN
src/android/app/src/main/res/drawable-xhdpi/button_turbo.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.5 KiB |
BIN
src/android/app/src/main/res/drawable-xxhdpi/button_turbo.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 6.9 KiB |
BIN
src/android/app/src/main/res/drawable-xxxhdpi/button_turbo.png
Normal file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.2 KiB |
|
@ -1,11 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<size
|
||||
android:width="1dp"
|
||||
android:height="1dp" />
|
||||
|
||||
<solid android:color="@color/citra_outlineVariant" />
|
||||
|
||||
</shape>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z"/>
|
||||
</vector>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M160,840q-33,0 -56.5,-23.5T80,760v-560q0,-33 23.5,-56.5T160,120h560q33,0 56.5,23.5T800,200v80h80v80h-80v80h80v80h-80v80h80v80h-80v80q0,33 -23.5,56.5T720,840L160,840ZM160,760h560v-560L160,200v560ZM240,680h200v-160L240,520v160ZM480,400h160v-120L480,280v120ZM240,480h200v-200L240,280v200ZM480,680h160v-240L480,440v240ZM160,200v560,-560Z"/>
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M160,840q-33,0 -56.5,-23.5T80,760v-560q0,-33 23.5,-56.5T160,120h560q33,0 56.5,23.5T800,200v80h80v80h-80v80h80v80h-80v80h80v80h-80v80q0,33 -23.5,56.5T720,840L160,840ZM160,760h560v-560L160,200v560ZM240,680h200v-160L240,520v160ZM480,400h160v-120L480,280v120ZM240,480h200v-200L240,280v200ZM480,680h160v-240L480,440v240ZM160,200v560,-560Z"/>
|
||||
</vector>
|
||||
|
|