Compare commits
118 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
|
@ -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"
|
||||
|
@ -20,9 +23,9 @@ cmake .. -G Ninja \
|
|||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
"${EXTRA_CMAKE_FLAGS[@]}" \
|
||||
-DENABLE_QT_TRANSLATION=ON \
|
||||
-DUSE_DISCORD_PRESENCE=ON
|
||||
-DUSE_DISCORD_PRESENCE=ON \
|
||||
"${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
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
#!/bin/bash -ex
|
||||
|
||||
if [ "$GITHUB_REF_TYPE" == "tag" ]; then
|
||||
export EXTRA_CMAKE_FLAGS=(-DENABLE_QT_UPDATE_CHECKER=ON)
|
||||
fi
|
||||
|
||||
mkdir build && cd build
|
||||
cmake .. -GNinja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
|
@ -7,7 +11,9 @@ cmake .. -GNinja \
|
|||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DENABLE_QT_TRANSLATION=ON \
|
||||
-DUSE_DISCORD_PRESENCE=ON
|
||||
-DENABLE_ROOM_STANDALONE=OFF \
|
||||
-DUSE_DISCORD_PRESENCE=ON \
|
||||
"${EXTRA_CMAKE_FLAGS[@]}"
|
||||
ninja
|
||||
ninja bundle
|
||||
mv ./bundle/azahar.app ./bundle/Azahar.app # TODO: Can this be done in CMake?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
#!/bin/sh -ex
|
||||
|
||||
mkdir build && cd build
|
||||
|
||||
if [ "$GITHUB_REF_TYPE" == "tag" ]; then
|
||||
export EXTRA_CMAKE_FLAGS=(-DENABLE_QT_UPDATE_CHECKER=ON)
|
||||
fi
|
||||
|
||||
cmake .. -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DENABLE_QT_TRANSLATION=ON \
|
||||
-DUSE_DISCORD_PRESENCE=ON
|
||||
-DUSE_DISCORD_PRESENCE=ON \
|
||||
"${EXTRA_CMAKE_FLAGS[@]}"
|
||||
ninja
|
||||
ninja bundle
|
||||
strip -s bundle/*.exe
|
||||
|
|
32
.github/workflows/build.yml
vendored
|
@ -49,12 +49,14 @@ jobs:
|
|||
${{ runner.os }}-${{ matrix.target }}-
|
||||
- name: Build
|
||||
run: ./.ci/linux.sh
|
||||
- name: Pack
|
||||
run: ./.ci/pack.sh
|
||||
- name: Move AppImage to artifacts directory
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
mv build/bundle/*.AppImage artifacts/
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
|
@ -148,19 +150,19 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.target }}-
|
||||
- name: Set up MSVC
|
||||
if: ${{ matrix.target == 'msvc' }}
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
if: ${{ matrix.target == 'msvc' }}
|
||||
- name: Install extra tools (MSVC)
|
||||
run: choco install ccache ninja wget
|
||||
if: ${{ matrix.target == 'msvc' }}
|
||||
run: choco install ccache ninja ptime wget
|
||||
- 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
|
||||
D:/a/_temp/vulkan.exe --accept-licenses --default-answer --confirm-command install
|
||||
if: ${{ matrix.target == 'msvc' }}
|
||||
- name: Set up MSYS2
|
||||
uses: msys2/setup-msys2@v2
|
||||
if: ${{ matrix.target == 'msys2' }}
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: clang64
|
||||
update: true
|
||||
|
@ -168,10 +170,16 @@ jobs:
|
|||
pacboy: >-
|
||||
toolchain:p ccache:p cmake:p ninja:p spirv-tools:p
|
||||
qt6-base:p qt6-multimedia:p qt6-multimedia-wmf:p qt6-tools:p qt6-translations:p
|
||||
- name: Install extra tools (MSYS2)
|
||||
if: ${{ matrix.target == 'msys2' }}
|
||||
uses: crazy-max/ghaction-chocolatey@v3
|
||||
with:
|
||||
args: install ptime wget
|
||||
- name: Install NSIS
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
run: |
|
||||
Invoke-WebRequest https://deac-riga.dl.sourceforge.net/project/nsis/NSIS%203/3.10/nsis-3.10-setup.exe?viasf=1 -OutFile C:\WINDOWS\Temp\nsis-3.10-setup.exe
|
||||
Invoke-Expression "& C:\WINDOWS\Temp\nsis-3.10-setup.exe \S"
|
||||
wget https://download.sourceforge.net/project/nsis/NSIS%203/3.11/nsis-3.11-setup.exe -O D:/a/_temp/nsis-setup.exe
|
||||
ptime D:/a/_temp/nsis-setup.exe /S
|
||||
shell: pwsh
|
||||
- name: Disable line ending translation
|
||||
run: git config --global core.autocrlf input
|
||||
|
@ -182,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
|
||||
|
@ -241,8 +249,8 @@ jobs:
|
|||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: src/android/app/artifacts/
|
||||
ios:
|
||||
runs-on: macos-14
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: macos-14
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
|
|
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"
|
2
.gitmodules
vendored
|
@ -6,7 +6,7 @@
|
|||
url = https://github.com/neobrain/nihstro.git
|
||||
[submodule "soundtouch"]
|
||||
path = externals/soundtouch
|
||||
url = https://codeberg.org/soundtouch/soundtouch.git
|
||||
url = https://github.com/azahar-emu/soundtouch.git
|
||||
[submodule "catch2"]
|
||||
path = externals/catch2
|
||||
url = https://github.com/catchorg/Catch2
|
||||
|
|
|
@ -41,8 +41,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()
|
||||
|
||||
|
@ -54,6 +54,13 @@ else()
|
|||
set(IS_RELEASE_BUILD ON)
|
||||
endif()
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options(
|
||||
/wd4711 # Suppresses `function 'xxxxx' selected for automatic inline expansion` messages
|
||||
/wd5045 # Suppresses `Compiler will insert Spectre mitigation for memory load if /Qspectre switch specified` messages
|
||||
)
|
||||
endif()
|
||||
|
||||
# LTO takes too much memory and time using MSVC.
|
||||
if (NOT MSVC AND IS_RELEASE_BUILD)
|
||||
set(DEFAULT_ENABLE_LTO ON)
|
||||
|
@ -61,6 +68,14 @@ else()
|
|||
set(DEFAULT_ENABLE_LTO OFF)
|
||||
endif()
|
||||
|
||||
# Disable OpenGL by default on Linux aarch64.
|
||||
# Some aarch64 devices running Linux don't support OpenGL, and users may encounter issues.
|
||||
if (LINUX AND CMAKE_SYSTEM_PROCESSOR STREQUAL aarch64)
|
||||
set(DEFAULT_ENABLE_OPENGL OFF)
|
||||
else()
|
||||
set(DEFAULT_ENABLE_OPENGL ON)
|
||||
endif()
|
||||
|
||||
option(ENABLE_SDL2 "Enable using SDL2" ON)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF)
|
||||
option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF)
|
||||
|
@ -71,7 +86,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)
|
||||
|
@ -82,7 +98,7 @@ option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)
|
|||
CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support" ON "NOT IOS" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_SOFTWARE_RENDERER "Enables the software renderer" ON "NOT ANDROID" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_OPENGL "Enables the OpenGL renderer" ON "NOT APPLE AND NOT (LINUX AND CMAKE_SYSTEM_PROCESSOR STREQUAL \"aarch64\")" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_OPENGL "Enables the OpenGL renderer" ${DEFAULT_ENABLE_OPENGL} "NOT APPLE" OFF)
|
||||
option(ENABLE_VULKAN "Enables the Vulkan renderer" ON)
|
||||
|
||||
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
||||
|
@ -103,6 +119,9 @@ 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()
|
||||
|
@ -469,9 +488,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()
|
||||
|
||||
|
@ -484,9 +502,14 @@ endif()
|
|||
# http://standards.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html
|
||||
if(ENABLE_QT AND UNIX AND NOT APPLE)
|
||||
install(FILES "${PROJECT_SOURCE_DIR}/dist/azahar.desktop"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/applications")
|
||||
install(FILES "${PROJECT_SOURCE_DIR}/dist/citra.svg"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/apps")
|
||||
install(FILES "${PROJECT_SOURCE_DIR}/dist/citra.xml"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/applications"
|
||||
RENAME "org.azahar_emu.Azahar.desktop")
|
||||
install(FILES "${PROJECT_SOURCE_DIR}/dist/azahar.svg"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/scalable/apps"
|
||||
RENAME "org.azahar_emu.Azahar.svg")
|
||||
install(FILES "${PROJECT_SOURCE_DIR}/dist/azahar.png"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/512x512/apps"
|
||||
RENAME "org.azahar_emu.Azahar.png")
|
||||
install(FILES "${PROJECT_SOURCE_DIR}/dist/org.azahar_emu.Azahar.xml"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/share/mime/packages")
|
||||
endif()
|
||||
|
|
|
@ -117,6 +117,9 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
set(extra_linuxdeploy_args --plugin qt)
|
||||
endif()
|
||||
|
||||
# Set up app icon
|
||||
file(COPY_FILE "${source_path}/dist/azahar.svg" "${CMAKE_BINARY_DIR}/dist/org.azahar_emu.Azahar.svg")
|
||||
|
||||
message(STATUS "Creating AppDir for executable ${executable_path}")
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E env
|
||||
${extra_linuxdeploy_env}
|
||||
|
@ -124,7 +127,7 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
${extra_linuxdeploy_args}
|
||||
--plugin checkrt
|
||||
--executable "${executable_path}"
|
||||
--icon-file "${source_path}/dist/citra.svg"
|
||||
--icon-file "${CMAKE_BINARY_DIR}/dist/org.azahar_emu.Azahar.svg"
|
||||
--desktop-file "${source_path}/dist/${executable_name}.desktop"
|
||||
--appdir "${appdir_path}"
|
||||
RESULT_VARIABLE linuxdeploy_appdir_result)
|
||||
|
@ -276,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/icon.png" "${CMAKE_BINARY_DIR}/bundle/dist/citra.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")
|
||||
|
@ -302,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()
|
||||
|
||||
|
|
26
README.md
|
@ -10,20 +10,20 @@ 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).
|
||||
|
@ -31,7 +31,7 @@ The recommended method of downloading Azahar on Android is via the [Google Play
|
|||
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`)
|
||||
2. Open Obtainium and click 'Add App'
|
||||
3. Type `https://github.com/azahar/azahar-emu` into the 'App Source URL' section
|
||||
3. Type `https://github.com/azahar-emu/azahar` into the 'App Source URL' section
|
||||
4. Click 'Add'
|
||||
5. Click 'Install'
|
||||
|
||||
|
@ -44,6 +44,8 @@ Keep in mind that you will not recieve automatic updates when installing via the
|
|||
|
||||
Azahar is available as an AppImage on the [Releases](https://github.com/azahar-emu/azahar/releases) page.
|
||||
|
||||
A Flatpak distribution is currently being worked on.
|
||||
<!--
|
||||
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>
|
||||
|
@ -84,13 +86,13 @@ 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
|
||||
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
|
||||
|
|
2
dist/azahar-room.desktop
vendored
|
@ -3,7 +3,7 @@ Version=1.0
|
|||
Type=Application
|
||||
Name=Azahar Room
|
||||
Comment=Multiplayer room host for Azahar
|
||||
Icon=citra
|
||||
Icon=org.azahar_emu.Azahar
|
||||
TryExec=azahar-room
|
||||
Exec=azahar-room %f
|
||||
Categories=Game;Emulator;
|
||||
|
|
2
dist/azahar.desktop
vendored
|
@ -6,7 +6,7 @@ GenericName=3DS Emulator
|
|||
GenericName[fr]=Émulateur 3DS
|
||||
Comment=Nintendo 3DS video game console emulator
|
||||
Comment[fr]=Émulateur de console de jeu Nintendo 3DS
|
||||
Icon=citra
|
||||
Icon=org.azahar_emu.Azahar
|
||||
TryExec=azahar
|
||||
Exec=azahar %f
|
||||
Categories=Game;Emulator;
|
||||
|
|
0
dist/citra.ico → dist/azahar.ico
vendored
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 462 KiB |
0
dist/citra.manifest → dist/azahar.manifest
vendored
BIN
dist/azahar.png
vendored
Normal file
After Width: | Height: | Size: 62 KiB |
0
dist/citra.svg → dist/azahar.svg
vendored
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
2
dist/compatibility_list
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 955c560089186a86a90b67f0427f6dfdabc1f177
|
||||
Subproject commit b153099f511759824941f5797ca69e2d39e57de3
|
BIN
dist/icon.png
vendored
Before Width: | Height: | Size: 50 KiB |
3
dist/languages/.tx/config
vendored
|
@ -6,9 +6,10 @@ file_filter = <lang>.ts
|
|||
source_file = en.ts
|
||||
source_lang = en
|
||||
type = QT
|
||||
lang_map = ca@valencia:ca_ES_valencia
|
||||
|
||||
[o:azahar:p:azahar:r:android]
|
||||
file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = ../../src/android/app/src/main/res/values/strings.xml
|
||||
type = ANDROID
|
||||
lang_map = es_ES:es, hu_HU:hu, ru_RU:ru, pt_BR:pt, zh_CN:zh, pl_PL:pl
|
||||
lang_map = es_ES:b+es+ES, hu_HU:b+hu+HU, ru_RU:b+ru+RU, pt_BR:b+pt+BR, zh_CN:b+zh+CN, pl_PL:b+pl+PL, ca@valencia:b+ca+ES+valencia, ko_KR:b+ko+KR, da_DK:b+da+DK, ja_JP:b+ja+JP, lt_LT:b+lt+LT, ro_RO:b+ro+RO, tr_TR:b+tr+TR, vi_VN:b+vi+VN, zh_TW:b+zh+TW
|
||||
|
|
7516
dist/languages/ca_ES_valencia.ts
vendored
Normal file
1257
dist/languages/da_DK.ts
vendored
1151
dist/languages/de.ts
vendored
1136
dist/languages/el.ts
vendored
1299
dist/languages/es_ES.ts
vendored
1136
dist/languages/fi.ts
vendored
1101
dist/languages/fr.ts
vendored
1136
dist/languages/hu_HU.ts
vendored
1136
dist/languages/id.ts
vendored
1762
dist/languages/it.ts
vendored
1239
dist/languages/ja_JP.ts
vendored
1136
dist/languages/ko_KR.ts
vendored
1140
dist/languages/lt_LT.ts
vendored
1136
dist/languages/nb.ts
vendored
1142
dist/languages/nl.ts
vendored
1101
dist/languages/pl_PL.ts
vendored
1161
dist/languages/pt_BR.ts
vendored
1144
dist/languages/ro_RO.ts
vendored
1136
dist/languages/ru_RU.ts
vendored
7521
dist/languages/sv.ts
vendored
Normal file
1180
dist/languages/tr_TR.ts
vendored
1136
dist/languages/vi_VN.ts
vendored
1159
dist/languages/zh_CN.ts
vendored
1136
dist/languages/zh_TW.ts
vendored
|
@ -4,7 +4,7 @@
|
|||
<comment>Nintendo 3DS homebrew executable</comment>
|
||||
<comment xml:lang="fr">Exécutable non-officiel pour Nintendo 3DS </comment>
|
||||
<acronym>3DSX</acronym>
|
||||
<icon name="citra"/>
|
||||
<icon name="azahar"/>
|
||||
<glob pattern="*.3dsx"/>
|
||||
<magic><match value="3DSX" type="string" offset="0"/></magic>
|
||||
</mime-type>
|
||||
|
@ -14,7 +14,7 @@
|
|||
<comment xml:lang="fr">Image de cartouche Nintendo 3DS</comment>
|
||||
<acronym>CCI</acronym>
|
||||
<expanded-acronym>CTR Cart Image</expanded-acronym>
|
||||
<icon name="citra"/>
|
||||
<icon name="azahar"/>
|
||||
<glob pattern="*.cci"/>
|
||||
<magic><match value="NCSD" type="string" offset="256"/></magic>
|
||||
</mime-type>
|
||||
|
@ -24,7 +24,7 @@
|
|||
<comment xml:lang="fr">Exécutable Nintendo 3DS</comment>
|
||||
<acronym>CXI</acronym>
|
||||
<expanded-acronym>CTR eXecutable Image</expanded-acronym>
|
||||
<icon name="citra"/>
|
||||
<icon name="azahar"/>
|
||||
<glob pattern="*.cxi"/>
|
||||
<magic><match value="NCCH" type="string" offset="256"/></magic>
|
||||
</mime-type>
|
||||
|
@ -34,7 +34,7 @@
|
|||
<comment xml:lang="fr">Archive installable Nintendo 3DS</comment>
|
||||
<acronym>CIA</acronym>
|
||||
<expanded-acronym>CTR Importable Archive</expanded-acronym>
|
||||
<icon name="citra"/>
|
||||
<icon name="azahar"/>
|
||||
<glob pattern="*.cia"/>
|
||||
</mime-type>
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
4
dist/qt_themes/default/theme_default.qrc
vendored
|
@ -13,7 +13,7 @@
|
|||
<file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file>
|
||||
<file alias="48x48/plus.png">icons/48x48/plus.png</file>
|
||||
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
|
||||
<file alias="256x256/citra.png">icons/256x256/citra.png</file>
|
||||
<file alias="256x256/azahar.png">icons/256x256/azahar.png</file>
|
||||
<file alias="48x48/star.png">icons/48x48/star.png</file>
|
||||
<file alias="256x256/plus_folder.png">icons/256x256/plus_folder.png</file>
|
||||
</qresource>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<file alias="48x48/no_avatar.png">icons_light/48x48/no_avatar.png</file>
|
||||
<file alias="48x48/plus.png">icons_light/48x48/plus.png</file>
|
||||
<file alias="48x48/sd_card.png">icons_light/48x48/sd_card.png</file>
|
||||
<file alias="256x256/citra.png">icons_light/256x256/citra.png</file>
|
||||
<file alias="256x256/azahar.png">icons_light/256x256/azahar.png</file>
|
||||
<file alias="48x48/star.png">icons_light/48x48/star.png</file>
|
||||
<file alias="256x256/plus_folder.png">icons_light/256x256/plus_folder.png</file>
|
||||
</qresource>
|
||||
|
|
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/discord-rpc
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 54eb03c20351f94850bfca3955cb87465a860ef6
|
||||
Subproject commit cb50201fc09290cd078c7ab27917504491f7f96a
|
2
externals/dynarmic
vendored
|
@ -1 +1 @@
|
|||
Subproject commit ef8380aef149fe39b2913c4a1678470db2eac1d4
|
||||
Subproject commit 278405bd71999ed3f3c77c5f78344a06fef798b9
|
2
externals/libressl
vendored
|
@ -1 +1 @@
|
|||
Subproject commit d4fc7348a3fbe9c659a373e28a3b50f052f7c50a
|
||||
Subproject commit 88b8e41b71099fabc57813bc06d8bc1aba050a19
|
2
externals/sdl2/SDL
vendored
|
@ -1 +1 @@
|
|||
Subproject commit e11183ea6caa3ae4895f4bc54cad2bbb0e365417
|
||||
Subproject commit 2359383fc187386204c3bb22de89655a494cd128
|
2
externals/sirit
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 4ab79a8c023aa63caaa93848b09b9fe8b183b1a9
|
||||
Subproject commit 37d49d2aa4c0a62f872720d6e5f2eaf90b2c95fa
|
2
externals/soundtouch
vendored
|
@ -1 +1 @@
|
|||
Subproject commit dd2252e9af3f2d6b749378173a4ae89551e06faf
|
||||
Subproject commit 9ef8458d8561d9471dd20e9619e3be4cfe564796
|
|
@ -115,7 +115,10 @@ else()
|
|||
-fstack-clash-protection
|
||||
)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
# If we define _FORTIFY_SOURCE when it is already defined, compilation will fail
|
||||
string(FIND "-D_FORTIFY_SOURCE" "${CMAKE_CXX_FLAGS} " FORTIFY_SOURCE_DEFINED)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL Debug AND NOT FORTIFY_SOURCE_DEFINED)
|
||||
# _FORTIFY_SOURCE can't be used without optimizations.
|
||||
add_compile_options(-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2)
|
||||
endif()
|
||||
|
@ -196,6 +199,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)
|
||||
|
|
|
@ -182,6 +182,14 @@ object NativeLibrary {
|
|||
|
||||
external fun uninstallSystemFiles(old3DS: Boolean)
|
||||
|
||||
external fun isFullConsoleLinked(): Boolean
|
||||
|
||||
external fun unlinkConsole()
|
||||
|
||||
external fun setTemporaryFrameLimit(speed: Double)
|
||||
|
||||
external fun disableTemporaryFrameLimit()
|
||||
|
||||
private var coreErrorAlertResult = false
|
||||
private val coreErrorAlertLock = Object()
|
||||
|
||||
|
@ -769,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 =
|
||||
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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -28,6 +29,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
|
||||
|
@ -44,7 +46,9 @@ import org.citra.citra_emu.databinding.CardGameBinding
|
|||
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
|
||||
|
||||
|
@ -203,6 +207,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)
|
||||
|
||||
|
@ -245,6 +360,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
|
||||
|
|
|
@ -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,14 @@ 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 }
|
||||
|
||||
fun handleHotkey(bindedButton: Int): Boolean {
|
||||
if(hotkeyButtons.contains(bindedButton)) {
|
||||
|
@ -22,16 +25,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.setTurboEnabled(!TurboHelper.isTurboSpeedEnabled())
|
||||
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
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ 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),
|
||||
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
|
||||
|
|
|
@ -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),
|
||||
|
@ -42,7 +43,7 @@ enum class IntSetting(
|
|||
PORTRAIT_BOTTOM_Y("custom_portrait_bottom_y",Settings.SECTION_LAYOUT,480),
|
||||
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("output_type", Settings.SECTION_AUDIO, 0),
|
||||
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),
|
||||
|
@ -60,11 +61,14 @@ enum class IntSetting(
|
|||
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);
|
||||
DISABLE_RIGHT_EYE_RENDER("disable_right_eye_render", Settings.SECTION_RENDERER, 0),
|
||||
TURBO_LIMIT("turbo_limit", Settings.SECTION_CORE, 200);
|
||||
|
||||
override var int: Int = defaultValue
|
||||
|
||||
override val valueAsString: String
|
||||
|
@ -93,7 +97,7 @@ enum class IntSetting(
|
|||
ASYNC_CUSTOM_LOADING,
|
||||
AUDIO_INPUT_TYPE,
|
||||
USE_ARTIC_BASE_CONTROLLER,
|
||||
SHADERS_ACCURATE_MUL,
|
||||
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.
|
||||
|
||||
|
@ -139,6 +139,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 +205,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 +215,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
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ class SwitchSetting(
|
|||
titleId: Int,
|
||||
descriptionId: Int,
|
||||
val key: String? = null,
|
||||
val defaultValue: Any? = null
|
||||
val defaultValue: Any? = null,
|
||||
override var isEnabled: Boolean = true
|
||||
) : SettingsItem(setting, titleId, descriptionId) {
|
||||
override val type = TYPE_SWITCH
|
||||
|
||||
|
|
|
@ -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() // TODO: Can this go someone 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,14 +65,13 @@ 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
|
||||
|
||||
class SettingsAdapter(
|
||||
private val fragmentView: SettingsFragmentView,
|
||||
private val context: Context
|
||||
public val context: Context
|
||||
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
|
||||
private var settings: ArrayList<SettingsItem>? = null
|
||||
private var clickedItem: SettingsItem? = null
|
||||
|
@ -153,15 +151,71 @@ class SettingsAdapter(
|
|||
return getItem(position)?.type ?: -1
|
||||
}
|
||||
|
||||
fun setSettingsList(settings: ArrayList<SettingsItem>?) {
|
||||
this.settings = settings ?: arrayListOf()
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import android.hardware.camera2.CameraManager
|
|||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlin.math.min
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
|
@ -236,6 +237,42 @@ 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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var countryCompatibilityChanged = true
|
||||
|
||||
private fun checkCountryCompatibility() {
|
||||
if (countryCompatibilityChanged) {
|
||||
countryCompatibilityChanged = false
|
||||
val compatFlags = SystemSaveGame.getCountryCompatibility(IntSetting.EMULATED_REGION.int)
|
||||
if (compatFlags != 0) {
|
||||
var message = ""
|
||||
if (compatFlags and 1 != 0) {
|
||||
message += settingsAdapter.context.getString(R.string.region_mismatch_emulated)
|
||||
}
|
||||
if (compatFlags and 2 != 0) {
|
||||
if (message.isNotEmpty()) message += "\n\n"
|
||||
message += settingsAdapter.context.getString(R.string.region_mismatch_console)
|
||||
}
|
||||
MaterialAlertDialogBuilder(settingsAdapter.context)
|
||||
.setTitle(R.string.region_mismatch)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,51 +319,45 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
)
|
||||
add(HeaderSetting(R.string.profile_settings))
|
||||
add(
|
||||
StringInputSetting(
|
||||
usernameSetting,
|
||||
R.string.username,
|
||||
0,
|
||||
"AZAHAR",
|
||||
10
|
||||
)
|
||||
)
|
||||
val regionSetting = object : AbstractIntSetting {
|
||||
override var int: Int
|
||||
get() {
|
||||
val ret = IntSetting.EMULATED_REGION.int
|
||||
checkCountryCompatibility()
|
||||
return ret
|
||||
}
|
||||
set(value) {
|
||||
IntSetting.EMULATED_REGION.int = value
|
||||
countryCompatibilityChanged = true
|
||||
checkCountryCompatibility()
|
||||
}
|
||||
override val key = IntSetting.EMULATED_REGION.key
|
||||
override val section = null
|
||||
override val isRuntimeEditable = false
|
||||
override val valueAsString get() = int.toString()
|
||||
override val defaultValue = IntSetting.EMULATED_REGION.defaultValue
|
||||
}
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.EMULATED_REGION,
|
||||
regionSetting,
|
||||
R.string.emulated_region,
|
||||
0,
|
||||
R.array.regionNames,
|
||||
R.array.regionValues,
|
||||
IntSetting.EMULATED_REGION.key,
|
||||
IntSetting.EMULATED_REGION.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
val systemLanguageSetting = object : AbstractIntSetting {
|
||||
override var int: Int
|
||||
get() = SystemSaveGame.getSystemLanguage()
|
||||
set(value) = SystemSaveGame.setSystemLanguage(value)
|
||||
override val key = null
|
||||
override val section = null
|
||||
override val isRuntimeEditable = false
|
||||
override val valueAsString get() = int.toString()
|
||||
override val defaultValue = 1
|
||||
}
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
systemLanguageSetting,
|
||||
R.string.emulated_language,
|
||||
0,
|
||||
R.array.languageNames,
|
||||
R.array.languageValues
|
||||
)
|
||||
)
|
||||
|
||||
val systemCountrySetting = object : AbstractShortSetting {
|
||||
override var short: Short
|
||||
get() = SystemSaveGame.getCountryCode()
|
||||
set(value) = SystemSaveGame.setCountryCode(value)
|
||||
get() {
|
||||
val ret = SystemSaveGame.getCountryCode()
|
||||
checkCountryCompatibility()
|
||||
return ret;
|
||||
}
|
||||
set(value) {
|
||||
SystemSaveGame.setCountryCode(value)
|
||||
countryCompatibilityChanged = true
|
||||
checkCountryCompatibility()
|
||||
}
|
||||
override val key = null
|
||||
override val section = null
|
||||
override val isRuntimeEditable = false
|
||||
|
@ -348,7 +379,34 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
countries.map { it.second }.toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
val systemLanguageSetting = object : AbstractIntSetting {
|
||||
override var int: Int
|
||||
get() = SystemSaveGame.getSystemLanguage()
|
||||
set(value) = SystemSaveGame.setSystemLanguage(value)
|
||||
override val key = null
|
||||
override val section = null
|
||||
override val isRuntimeEditable = false
|
||||
override val valueAsString get() = int.toString()
|
||||
override val defaultValue = 1
|
||||
}
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
systemLanguageSetting,
|
||||
R.string.emulated_language,
|
||||
0,
|
||||
R.array.languageNames,
|
||||
R.array.languageValues
|
||||
)
|
||||
)
|
||||
add(
|
||||
StringInputSetting(
|
||||
usernameSetting,
|
||||
R.string.username,
|
||||
0,
|
||||
"AZAHAR",
|
||||
10
|
||||
)
|
||||
)
|
||||
val playCoinSettings = object : AbstractIntSetting {
|
||||
override var int: Int
|
||||
get() = SystemSaveGame.getPlayCoins()
|
||||
|
@ -480,7 +538,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
|
||||
)
|
||||
|
@ -868,7 +926,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()
|
||||
|
@ -951,6 +1009,19 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
)
|
||||
|
||||
add(HeaderSetting(R.string.advanced))
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.TEXTURE_SAMPLING,
|
||||
R.string.texture_sampling_name,
|
||||
R.string.texture_sampling_description,
|
||||
R.array.textureSamplingNames,
|
||||
R.array.textureSamplingValues,
|
||||
IntSetting.TEXTURE_SAMPLING.key,
|
||||
IntSetting.TEXTURE_SAMPLING.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
// Disabled until custom texture implementation gets rewrite, current one overloads RAM
|
||||
// and crashes Citra.
|
||||
// add(
|
||||
|
@ -979,6 +1050,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,
|
||||
|
@ -1012,6 +1092,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,
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -22,6 +22,7 @@ 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
|
||||
|
@ -232,8 +233,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 -> {
|
||||
|
@ -447,6 +455,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
Choreographer.getInstance().postFrameCallback(this)
|
||||
if (NativeLibrary.isRunning()) {
|
||||
NativeLibrary.unPauseEmulation()
|
||||
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 +549,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 +557,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 +575,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)
|
||||
|
@ -886,10 +902,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 +920,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 +954,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 +1137,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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -6,10 +6,12 @@ package org.citra.citra_emu.fragments
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
@ -22,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -33,6 +36,8 @@ 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
|
||||
|
@ -40,6 +45,7 @@ class GamesFragment : Fragment() {
|
|||
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private var show3DSFileWarning: Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -143,6 +149,34 @@ class GamesFragment : Fragment() {
|
|||
setInsets()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (show3DSFileWarning &&
|
||||
!PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
.getBoolean("show_3ds_files_warning", false)) {
|
||||
val message = HtmlCompat.fromHtml(getString(R.string.warning_3ds_files),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
|
||||
context?.let {
|
||||
val alert = MaterialAlertDialogBuilder(it)
|
||||
.setTitle(getString(R.string.important))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.dont_show_again) { _, _ ->
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
.edit() {
|
||||
putBoolean("show_3ds_files_warning", true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
val alertMessage = alert.findViewById<View>(android.R.id.message) as TextView
|
||||
alertMessage.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
show3DSFileWarning = false
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -22,9 +22,10 @@ class SelectUserDirectoryDialogFragment : DialogFragment() {
|
|||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
isCancelable = false
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.select_citra_user_folder)
|
||||
.setMessage(R.string.cannot_skip_directory_description)
|
||||
.setMessage(R.string.selecting_user_directory_without_write_permissions)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
mainActivity?.openCitraDirectory?.launch(null)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package org.citra.citra_emu.fragments
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.Gravity
|
||||
|
@ -157,6 +158,22 @@ class SystemFilesFragment : Fragment() {
|
|||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
|
||||
binding.buttonUnlinkConsoleData.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.delete_system_files)
|
||||
.setMessage(HtmlCompat.fromHtml(
|
||||
requireContext().getString(R.string.delete_system_files_description),
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
))
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
NativeLibrary.unlinkConsole()
|
||||
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
binding.buttonSetUpSystemFiles.setOnClickListener {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
// 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.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RadioButton
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.ui.main.MainActivity
|
||||
import org.citra.citra_emu.utils.CitraDirectoryUtils
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
|
||||
class UpdateUserDirectoryDialogFragment : DialogFragment() {
|
||||
private lateinit var mainActivity: MainActivity
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
isCancelable = false
|
||||
val preferences: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
val ld = preferences.getString("LIME3DS_DIRECTORY","")
|
||||
val cd = preferences.getString("CITRA_DIRECTORY","")
|
||||
val dialogView = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.dialog_select_which_directory, null)
|
||||
|
||||
val radioGroup = dialogView.findViewById<RadioGroup>(R.id.radioGroup)
|
||||
|
||||
val choices = listOf(
|
||||
getString(R.string.keep_current_azahar_directory) to Uri.parse(cd).path,
|
||||
getString(R.string.use_prior_lime3ds_directory) to Uri.parse(ld).path
|
||||
)
|
||||
var selected = -1 // 0 = current, 1 = prior, -1 = no selection
|
||||
|
||||
choices.forEachIndexed { index, (label, subtext) ->
|
||||
val container = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(0, 16, 0, 16)
|
||||
}
|
||||
|
||||
val radioButton = RadioButton(requireContext()).apply {
|
||||
text = label
|
||||
id = View.generateViewId()
|
||||
}
|
||||
|
||||
val subTextView = TextView(requireContext()).apply {
|
||||
text = subtext
|
||||
setPadding(64, 4, 0, 0) // indent for visual hierarchy
|
||||
setTextAppearance(android.R.style.TextAppearance_Small)
|
||||
}
|
||||
|
||||
container.addView(radioButton)
|
||||
container.addView(subTextView)
|
||||
radioGroup.addView(container)
|
||||
|
||||
// RadioGroup expects RadioButtons directly, so we need to manage selection ourselves
|
||||
radioButton.setOnClickListener {
|
||||
selected = index
|
||||
// Manually uncheck others
|
||||
for (i in 0 until radioGroup.childCount) {
|
||||
val child = radioGroup.getChildAt(i) as LinearLayout
|
||||
val rb = child.getChildAt(0) as RadioButton
|
||||
rb.isChecked = i == index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.select_citra_user_folder)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
if (selected == 1) {
|
||||
PermissionsHandler.setCitraDirectory(ld)
|
||||
}
|
||||
if (selected >= 0) {
|
||||
CitraDirectoryUtils.removeLimeDirectoryPreference()
|
||||
DirectoryInitialization.resetCitraDirectoryState()
|
||||
DirectoryInitialization.start()
|
||||
}
|
||||
|
||||
ViewModelProvider(mainActivity)[HomeViewModel::class.java].setPickingUserDir(false)
|
||||
ViewModelProvider(mainActivity)[HomeViewModel::class.java].setUserDir(this.requireActivity(),PermissionsHandler.citraDirectory.path!!)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "UpdateUserDirectoryDialogFragment"
|
||||
|
||||
fun newInstance(activity: FragmentActivity): UpdateUserDirectoryDialogFragment {
|
||||
ViewModelProvider(activity)[HomeViewModel::class.java].setPickingUserDir(true)
|
||||
return UpdateUserDirectoryDialogFragment()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
||||
|
@ -22,6 +22,8 @@ class GameInfo(path: String) {
|
|||
|
||||
external fun getTitle(): String
|
||||
|
||||
external fun isEncrypted(): Boolean
|
||||
|
||||
external fun getRegions(): String
|
||||
|
||||
external fun getCompany(): String
|
||||
|
|
|
@ -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.setTurboEnabled((!TurboHelper.isTurboSpeedEnabled()))
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -34,10 +34,8 @@ import androidx.work.OutOfQuotaPolicy
|
|||
import androidx.work.WorkManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.activities.EmulationActivity
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||
import org.citra.citra_emu.databinding.ActivityMainBinding
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
|
@ -45,8 +43,10 @@ import org.citra.citra_emu.features.settings.model.SettingsViewModel
|
|||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment
|
||||
import org.citra.citra_emu.fragments.UpdateUserDirectoryDialogFragment
|
||||
import org.citra.citra_emu.utils.CiaInstallWorker
|
||||
import org.citra.citra_emu.utils.CitraDirectoryHelper
|
||||
import org.citra.citra_emu.utils.CitraDirectoryUtils
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper
|
||||
import org.citra.citra_emu.utils.InsetsHelper
|
||||
|
@ -66,13 +66,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
CitraDirectoryUtils.attemptAutomaticUpdateDirectory()
|
||||
splashScreen.setKeepOnScreenCondition {
|
||||
!DirectoryInitialization.areCitraDirectoriesReady() &&
|
||||
PermissionsHandler.hasWriteAccess(this)
|
||||
PermissionsHandler.hasWriteAccess(this) &&
|
||||
!CitraDirectoryUtils.needToUpdateManually()
|
||||
}
|
||||
|
||||
|
||||
if (PermissionsHandler.hasWriteAccess(applicationContext) &&
|
||||
DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
DirectoryInitialization.areCitraDirectoriesReady() &&
|
||||
!CitraDirectoryUtils.needToUpdateManually()) {
|
||||
settingsViewModel.settings.loadSettings()
|
||||
}
|
||||
|
||||
|
@ -185,6 +189,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
) {
|
||||
SelectUserDirectoryDialogFragment.newInstance(this)
|
||||
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||
} else if (!firstTimeSetup && !homeViewModel.isPickingUserDir.value && CitraDirectoryUtils.needToUpdateManually()) {
|
||||
UpdateUserDirectoryDialogFragment.newInstance(this)
|
||||
.show(supportFragmentManager,UpdateUserDirectoryDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
// 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.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
|
||||
object CitraDirectoryUtils {
|
||||
const val CITRA_DIRECTORY = "CITRA_DIRECTORY"
|
||||
const val LIME3DS_DIRECTORY = "LIME3DS_DIRECTORY"
|
||||
val preferences: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
|
||||
fun needToUpdateManually(): Boolean {
|
||||
val directoryString = preferences.getString(CITRA_DIRECTORY, "")
|
||||
val limeDirectoryString = preferences.getString(LIME3DS_DIRECTORY,"")
|
||||
return (directoryString != "" && limeDirectoryString != "" && directoryString != limeDirectoryString)
|
||||
}
|
||||
|
||||
fun attemptAutomaticUpdateDirectory() {
|
||||
val directoryString = preferences.getString(CITRA_DIRECTORY, "")
|
||||
val limeDirectoryString = preferences.getString(LIME3DS_DIRECTORY,"")
|
||||
if (needToUpdateManually()) {
|
||||
return;
|
||||
}
|
||||
if (directoryString == "" && limeDirectoryString != "") {
|
||||
// Upgrade from Lime3DS to Azahar
|
||||
PermissionsHandler.setCitraDirectory(limeDirectoryString)
|
||||
removeLimeDirectoryPreference()
|
||||
DirectoryInitialization.resetCitraDirectoryState()
|
||||
DirectoryInitialization.start()
|
||||
|
||||
} else if (directoryString != "" && directoryString == limeDirectoryString) {
|
||||
// Both the Lime3DS and Azahar directories are the same,
|
||||
// so delete the obsolete Lime3DS value.
|
||||
removeLimeDirectoryPreference()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLimeDirectoryPreference() {
|
||||
preferences.edit().remove(LIME3DS_DIRECTORY).apply()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,6 +11,7 @@ import kotlinx.serialization.encodeToString
|
|||
import kotlinx.serialization.json.Json
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.model.CheapDocument
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.model.GameInfo
|
||||
|
@ -69,19 +70,25 @@ object GameHelper {
|
|||
|
||||
fun getGame(uri: Uri, isInstalled: Boolean, addedToLibrary: Boolean): Game {
|
||||
val filePath = uri.toString()
|
||||
val gameInfo: GameInfo? = try {
|
||||
var gameInfo: GameInfo? = try {
|
||||
GameInfo(filePath)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
var isEncrypted = false
|
||||
if (gameInfo?.isEncrypted() == true) {
|
||||
gameInfo = null
|
||||
isEncrypted = true
|
||||
}
|
||||
|
||||
val newGame = Game(
|
||||
(gameInfo?.getTitle() ?: FileUtil.getFilename(uri)).replace("[\\t\\n\\r]+".toRegex(), " "),
|
||||
filePath.replace("\n", " "),
|
||||
filePath,
|
||||
NativeLibrary.getTitleId(filePath),
|
||||
gameInfo?.getCompany() ?: "",
|
||||
gameInfo?.getRegions() ?: "Invalid region",
|
||||
gameInfo?.getRegions() ?: (if (isEncrypted) { CitraApplication.appContext.getString(R.string.unsupported_encrypted) } else { CitraApplication.appContext.getString(R.string.invalid_region) }),
|
||||
isInstalled,
|
||||
NativeLibrary.getIsSystemTitle(filePath),
|
||||
gameInfo?.getIsVisibleSystemTitle() ?: false,
|
||||
|
|
|
@ -48,6 +48,8 @@ object SystemSaveGame {
|
|||
external fun getMac(): String
|
||||
|
||||
external fun regenerateMac()
|
||||
|
||||
external fun getCountryCompatibility(region: Int): Int
|
||||
}
|
||||
|
||||
enum class BirthdayMonth(val code: Int, val days: Int) {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
// 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 setTurboEnabled(state: Boolean) {
|
||||
turboSpeedEnabled = state
|
||||
reloadTurbo()
|
||||
}
|
||||
|
||||
fun reloadTurbo() {
|
||||
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)
|
||||
}
|
||||
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
|
@ -149,7 +149,7 @@ void Config::ReadValues() {
|
|||
ReadSetting("Renderer", Settings::values.use_vsync_new);
|
||||
ReadSetting("Renderer", Settings::values.texture_filter);
|
||||
ReadSetting("Renderer", Settings::values.texture_sampling);
|
||||
|
||||
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);
|
||||
|
@ -183,11 +183,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);
|
||||
|
|
|
@ -125,6 +125,11 @@ shaders_accurate_mul =
|
|||
# 0: Interpreter (slow), 1 (default): JIT (fast)
|
||||
use_shader_jit =
|
||||
|
||||
# Overrides the sampling filter used by games. This can be useful in certain
|
||||
# cases with poorly behaved games when upscaling.
|
||||
# 0 (default): Game Controlled, 1: Nearest Neighbor, 2: Linear
|
||||
texture_sampling =
|
||||
|
||||
# Forces VSync on the display thread. Usually doesn't impact performance, but on some drivers it can
|
||||
# so only turn this off if you notice a speed difference.
|
||||
# 0: Off, 1 (default): On
|
||||
|
@ -151,6 +156,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 =
|
||||
|
@ -162,7 +171,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.
|
||||
|
@ -197,6 +206,12 @@ disable_right_eye_render =
|
|||
# 5: Custom Layout
|
||||
layout_option =
|
||||
|
||||
# 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 =
|
||||
|
@ -251,6 +266,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 =
|
||||
|
|