mirror of
https://github.com/azahar-emu/azahar.git
synced 2026-06-10 04:33:40 -04:00
Merge branch 'master' into COMBO_BUTTON
This commit is contained in:
commit
2ef7c17799
346 changed files with 29976 additions and 18556 deletions
|
|
@ -9,8 +9,14 @@ fi
|
|||
|
||||
cd src/android
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease
|
||||
./gradlew bundleRelease
|
||||
|
||||
if [[ "$TARGET" == "googleplay" ]]; then
|
||||
./gradlew assembleGooglePlayRelease
|
||||
./gradlew bundleGooglePlayRelease
|
||||
else
|
||||
./gradlew assembleVanillaRelease
|
||||
./gradlew bundleVanillaRelease
|
||||
fi
|
||||
|
||||
ccache -s -v
|
||||
|
||||
|
|
|
|||
17
.ci/docker.sh
Executable file
17
.ci/docker.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash -ex
|
||||
|
||||
GITREV="`git show -s --format='%h'`" || true
|
||||
|
||||
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
|
||||
TAG_NAME=$GITHUB_REF_NAME
|
||||
elif [[ -n $GITREV ]]; then
|
||||
TAG_NAME=$GITREV
|
||||
else
|
||||
TAG_NAME=unknown
|
||||
fi
|
||||
|
||||
echo "Tag name is: $TAG_NAME"
|
||||
|
||||
docker build -f docker/azahar-room/Dockerfile -t azahar-room:$TAG_NAME .
|
||||
mkdir -p build
|
||||
docker save azahar-room:$TAG_NAME > build/azahar-room-$TAG_NAME.dockerimage
|
||||
12
.ci/linux.sh
12
.ci/linux.sh
|
|
@ -1,14 +1,16 @@
|
|||
#!/bin/bash -ex
|
||||
|
||||
if [ "$TARGET" = "appimage" ]; then
|
||||
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
|
||||
-DENABLE_ROOM_STANDALONE=OFF)
|
||||
# Bundle required QT wayland libraries
|
||||
export EXTRA_QT_PLUGINS="waylandcompositor"
|
||||
export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so"
|
||||
if [ "$TARGET" = "appimage-wayland" ]; then
|
||||
# Bundle required QT wayland libraries
|
||||
export EXTRA_QT_PLUGINS="waylandcompositor"
|
||||
export EXTRA_PLATFORM_PLUGINS="libqwayland-egl.so;libqwayland-generic.so"
|
||||
fi
|
||||
else
|
||||
# For the linux-fresh verification target, verify compilation without PCH as well.
|
||||
export EXTRA_CMAKE_FLAGS=(-DCITRA_USE_PRECOMPILED_HEADERS=OFF)
|
||||
|
|
@ -30,7 +32,7 @@ cmake .. -G Ninja \
|
|||
ninja
|
||||
strip -s bin/Release/*
|
||||
|
||||
if [ "$TARGET" = "appimage" ]; then
|
||||
if [[ "$TARGET" == "appimage"* ]]; then
|
||||
ninja bundle
|
||||
# TODO: Our AppImage environment currently uses an older ccache version without the verbose flag.
|
||||
ccache -s
|
||||
|
|
|
|||
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
<!--
|
||||
If you are contributing to Azahar for the first time please
|
||||
keep the block of text between `---` and write your
|
||||
PR description below it. Do not write anything inside
|
||||
or change this block of text!
|
||||
|
||||
If you are a recurrent contributor, remove this entire
|
||||
block of text and proceed as normal.
|
||||
-->
|
||||
|
||||

|
||||
---
|
||||
BIN
.github/ignore_unless_human.png
vendored
Normal file
BIN
.github/ignore_unless_human.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
95
.github/workflows/build.yml
vendored
95
.github/workflows/build.yml
vendored
|
|
@ -22,11 +22,13 @@ jobs:
|
|||
with:
|
||||
name: source
|
||||
path: artifacts/
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ["appimage", "fresh"]
|
||||
target: ["appimage", "appimage-wayland", "fresh"]
|
||||
container:
|
||||
image: opensauce04/azahar-build-environment:latest
|
||||
options: -u 1001
|
||||
|
|
@ -36,11 +38,14 @@ jobs:
|
|||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: linux
|
||||
TARGET: ${{ matrix.target }}
|
||||
SHOULD_RUN: ${{ (matrix.target != 'appimage-wayland' || github.ref_type == 'tag') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
|
|
@ -48,21 +53,28 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.target }}-
|
||||
- name: Build
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: ./.ci/linux.sh
|
||||
- name: Move AppImage to artifacts directory
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
if: ${{ contains(matrix.target, 'appimage') && env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
mv build/bundle/*.AppImage artifacts/
|
||||
- name: Rename AppImage
|
||||
if: ${{ matrix.target == 'appimage-wayland' && env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
mv artifacts/azahar.AppImage artifacts/azahar-wayland.AppImage
|
||||
- name: Upload
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
if: ${{ contains(matrix.target, 'appimage') && env.SHOULD_RUN == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
|
||||
macos:
|
||||
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-13') || 'macos-14' }}
|
||||
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-15-intel') || 'macos-26' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ["x86_64", "arm64"]
|
||||
env:
|
||||
|
|
@ -87,14 +99,22 @@ jobs:
|
|||
- name: Build
|
||||
run: ./.ci/macos.sh
|
||||
- name: Prepare outputs for caching
|
||||
run: mv build/bundle $OS-$TARGET
|
||||
run: cp -R build/bundle $OS-$TARGET
|
||||
- name: Cache outputs for universal build
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ${{ env.OS }}-${{ env.TARGET }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
- name: Pack
|
||||
run: ./.ci/pack.sh
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
|
||||
macos-universal:
|
||||
runs-on: macos-14
|
||||
runs-on: macos-26
|
||||
needs: macos
|
||||
env:
|
||||
OS: macos
|
||||
|
|
@ -124,9 +144,11 @@ jobs:
|
|||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ["msvc", "msys2"]
|
||||
defaults:
|
||||
|
|
@ -200,79 +222,92 @@ jobs:
|
|||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ["vanilla", "googleplay"]
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: android
|
||||
TARGET: universal
|
||||
TARGET: ${{ matrix.target }}
|
||||
SHOULD_RUN: ${{ (matrix.target == 'vanilla' || github.ref_type == 'tag') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
${{ env.CCACHE_DIR }}
|
||||
key: ${{ runner.os }}-android-${{ github.sha }}
|
||||
key: ${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-android-
|
||||
${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}-
|
||||
- name: Set tag name
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
|
||||
echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
|
||||
fi
|
||||
echo $GIT_TAG_NAME
|
||||
- name: Deps
|
||||
- name: Install tools
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install ccache apksigner -y
|
||||
- name: Update Android SDK CMake version
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
|
||||
- name: Build
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
||||
env:
|
||||
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
|
||||
- name: Pack
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: ../../../.ci/pack.sh
|
||||
working-directory: src/android/app
|
||||
env:
|
||||
UNPACKED: 1
|
||||
- name: Upload
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: src/android/app/artifacts/
|
||||
ios:
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: macos-14
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: ios
|
||||
TARGET: arm64
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: docker:dind
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ${{ runner.os }}-ios-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ios-
|
||||
- name: Install tools
|
||||
run: brew install ccache ninja
|
||||
- name: Build
|
||||
run: ./.ci/ios.sh
|
||||
|
||||
run: apk add bash
|
||||
- name: Fix git ownership
|
||||
run: git config --global --add safe.directory .
|
||||
- name: Build Docker image
|
||||
run: ./.ci/docker.sh
|
||||
- name: Move Docker image to artifacts directory
|
||||
run: |
|
||||
mkdir -p artifacts
|
||||
mv build/*.dockerimage artifacts/
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker
|
||||
path: artifacts/
|
||||
51
.github/workflows/first_time_contributor_detect.yml
vendored
Normal file
51
.github/workflows/first_time_contributor_detect.yml
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
name: Detect first-time contributors
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
detect:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
(github.repository == 'azahar-emu/azahar') &&
|
||||
(github.event.pull_request.author_association != 'COLLABORATOR') &&
|
||||
(github.event.pull_request.author_association != 'CONTRIBUTOR') &&
|
||||
(github.event.pull_request.author_association != 'MANNEQUIN') &&
|
||||
(github.event.pull_request.author_association != 'MEMBER') &&
|
||||
(github.event.pull_request.author_association != 'OWNER')
|
||||
steps:
|
||||
- name: Detect PR if author is first-time contributor
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Add needs verification label so that the reopen action runs on comment.
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
labels: ['needs verification'],
|
||||
});
|
||||
|
||||
// Close the pull request and wait for verification.
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
state: 'closed',
|
||||
});
|
||||
|
||||
// Show the new contributor how to verify (they need to write a short poem about the Wii and 3DS being lovers)
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: 'Welcome to the Azahar Emulator repository! Due to the surge of AI bots we have decided to add an extra verification step to new contributors. Please follow the exact instructions in your own written Pull Request description to reopen it.',
|
||||
});
|
||||
79
.github/workflows/first_time_contributor_reopen.yml
vendored
Normal file
79
.github/workflows/first_time_contributor_reopen.yml
vendored
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
name: Verify first-time contributors
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.issue.pull_request && contains(github.event.issue.labels.*.name, 'needs verification')
|
||||
steps:
|
||||
- name: Verify and reopen PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const issue = context.payload.issue;
|
||||
const comment = context.payload.comment;
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue.number,
|
||||
});
|
||||
|
||||
// Only allow verification of the comment user is the author
|
||||
if (comment.user.login !== pr.user.login) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch user display and login names (lowercase)
|
||||
const { data: user } = await github.rest.users.getByUsername({
|
||||
username: pr.user.login,
|
||||
});
|
||||
const username = pr.user.login.toLowerCase();
|
||||
const displayName = (user.name || '').toLowerCase();
|
||||
|
||||
// Make comment body lowercase and split words
|
||||
const body = comment.body.toLowerCase().trim().replace(/[^a-z0-9_\-\s]/g, '').split(/\s+/);
|
||||
|
||||
// Check that the user verified themselves by writing a song about the NES and the SNES.
|
||||
const verified =
|
||||
(body.includes(username) ||
|
||||
(displayName && body.includes(displayName))) &&
|
||||
body.includes('azahar');
|
||||
|
||||
// Only reopen the PR and remove the label if verification succeeded
|
||||
if (verified) {
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue.number,
|
||||
state: 'open',
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
body: 'Verification successful! Pull request has been reopened. Please also edit your PR description to remove the block of text between `---` to make the description easier to read.',
|
||||
});
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
name: 'needs verification',
|
||||
});
|
||||
} catch {}
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
body: 'Verification failed! Pull request will remain closed.',
|
||||
});
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -10,6 +10,9 @@ src/installer/*.exe
|
|||
src/common/scm_rev.cpp
|
||||
.travis.descriptor.json
|
||||
|
||||
# Docker image files
|
||||
*.dockerimage
|
||||
|
||||
# Project/editor files
|
||||
*.swp
|
||||
*.kdev4
|
||||
|
|
|
|||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -100,3 +100,6 @@
|
|||
[submodule "spirv-headers"]
|
||||
path = externals/spirv-headers
|
||||
url = https://github.com/KhronosGroup/SPIRV-Headers
|
||||
[submodule "externals/xxHash"]
|
||||
path = externals/xxHash
|
||||
url = https://github.com/Cyan4973/xxHash.git
|
||||
|
|
|
|||
|
|
@ -48,6 +48,15 @@ if (APPLE)
|
|||
else()
|
||||
# Minimum macOS 13
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.4")
|
||||
|
||||
# Catch compiler issue on AppleClang versions below 15.0
|
||||
# TODO: Remove this check when we drop macOS 13 Ventura
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" AND
|
||||
CMAKE_CXX_COMPILER_VERSION VERSION_LESS 15.0)
|
||||
message(FATAL_ERROR "AppleClang 15.0 or later is required due to a compiler bug in earlier versions.\n"
|
||||
"Current version: ${CMAKE_CXX_COMPILER_VERSION}\n"
|
||||
"After updating, delete 'CMakeCache.txt' in the build directory.")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
@ -112,6 +121,8 @@ option(ENABLE_MICROPROFILE "Enables microprofile capabilities" OFF)
|
|||
|
||||
option(ENABLE_SSE42 "Enable SSE4.2 optimizations on x86_64" ON)
|
||||
|
||||
option(ENABLE_DEVELOPER_OPTIONS "Enable functionality targeted at emulator developers" OFF)
|
||||
|
||||
# Compile options
|
||||
CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF)
|
||||
option(ENABLE_LTO "Enable link time optimization" ${DEFAULT_ENABLE_LTO})
|
||||
|
|
@ -212,7 +223,7 @@ function(check_submodules_present)
|
|||
foreach(module ${gitmodules})
|
||||
string(REGEX REPLACE "path *= *" "" module ${module})
|
||||
if (NOT EXISTS "${PROJECT_SOURCE_DIR}/${module}/.git")
|
||||
message(SEND_ERROR "Git submodule ${module} not found."
|
||||
message(SEND_ERROR "Git submodule ${module} not found.\n"
|
||||
"Please run: git submodule update --init --recursive")
|
||||
endif()
|
||||
endforeach()
|
||||
|
|
@ -299,7 +310,7 @@ find_package(Threads REQUIRED)
|
|||
|
||||
if (ENABLE_QT)
|
||||
if (NOT USE_SYSTEM_QT)
|
||||
download_qt(6.7.2)
|
||||
download_qt(6.9.3)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent)
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# To use this as a script, make sure you pass in the variables BASE_DIR, SRC_DIR, BUILD_DIR, and TARGET_FILE
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
|
||||
if(WIN32)
|
||||
set(PLATFORM "windows")
|
||||
elseif(APPLE)
|
||||
set(PLATFORM "mac")
|
||||
elseif(UNIX)
|
||||
set(PLATFORM "linux")
|
||||
else()
|
||||
message(FATAL_ERROR "Cannot build installer for this unsupported platform")
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${BASE_DIR}/CMakeModules")
|
||||
include(DownloadExternals)
|
||||
download_qt(tools_ifw)
|
||||
get_external_prefix(qt QT_PREFIX)
|
||||
|
||||
file(GLOB_RECURSE INSTALLER_BASE "${QT_PREFIX}/**/installerbase*")
|
||||
file(GLOB_RECURSE BINARY_CREATOR "${QT_PREFIX}/**/binarycreator*")
|
||||
|
||||
set(CONFIG_FILE "${SRC_DIR}/config/config_${PLATFORM}.xml")
|
||||
set(PACKAGES_DIR "${BUILD_DIR}/packages")
|
||||
file(MAKE_DIRECTORY ${PACKAGES_DIR})
|
||||
|
||||
execute_process(COMMAND ${BINARY_CREATOR} -t ${INSTALLER_BASE} -n -c ${CONFIG_FILE} -p ${PACKAGES_DIR} ${TARGET_FILE})
|
||||
|
|
@ -130,25 +130,10 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
--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)
|
||||
RESULT_VARIABLE linuxdeploy_appdir_result
|
||||
ERROR_VARIABLE linuxdeploy_appdir_error)
|
||||
if (NOT linuxdeploy_appdir_result EQUAL "0")
|
||||
message(FATAL_ERROR "linuxdeploy failed to create AppDir: ${linuxdeploy_appdir_result}")
|
||||
endif()
|
||||
|
||||
if (enable_qt)
|
||||
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
|
||||
file(READ "${qt_hook_file}" qt_hook_contents)
|
||||
# Add Cinnamon to list of DEs for GTK3 theming.
|
||||
string(REPLACE
|
||||
"*XFCE*"
|
||||
"*X-Cinnamon*|*XFCE*"
|
||||
qt_hook_contents "${qt_hook_contents}")
|
||||
# Wayland backend crashes due to changed schemas in Gnome 40.
|
||||
string(REPLACE
|
||||
"export QT_QPA_PLATFORMTHEME=gtk3"
|
||||
"export QT_QPA_PLATFORMTHEME=gtk3; export GDK_BACKEND=x11"
|
||||
qt_hook_contents "${qt_hook_contents}")
|
||||
file(WRITE "${qt_hook_file}" "${qt_hook_contents}")
|
||||
message(FATAL_ERROR "linuxdeploy failed to create AppDir w/ exit code ${linuxdeploy_appdir_result}:\n${linuxdeploy_appdir_error}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Creating AppImage for executable ${executable_path}")
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out
|
|||
set(arch_path "mingw_64")
|
||||
elseif (MSVC)
|
||||
if ("arm64" IN_LIST ARCHITECTURE)
|
||||
set(arch_path "msvc2019_arm64")
|
||||
set(arch_path "msvc2022_arm64")
|
||||
elseif ("x86_64" IN_LIST ARCHITECTURE)
|
||||
set(arch_path "msvc2019_64")
|
||||
set(arch_path "msvc2022_64")
|
||||
else()
|
||||
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
|
||||
endif()
|
||||
|
|
@ -30,12 +30,13 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out
|
|||
|
||||
# In case we're cross-compiling, prepare to also fetch the correct host Qt tools.
|
||||
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64")
|
||||
set(host_arch_path "msvc2019_64")
|
||||
set(host_arch_path "msvc2022_64")
|
||||
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||
# TODO: msvc2019_arm64 doesn't include some of the required tools for some reason,
|
||||
# TODO: so until it does, just use msvc2019_64 under x86_64 emulation.
|
||||
# TODO: ^ Is this still true with msvc2022?
|
||||
# set(host_arch_path "msvc2019_arm64")
|
||||
set(host_arch_path "msvc2019_64")
|
||||
set(host_arch_path "msvc2022_64")
|
||||
endif()
|
||||
set(host_arch "win64_${host_arch_path}")
|
||||
else()
|
||||
|
|
@ -105,7 +106,7 @@ function(download_qt_configuration prefix_out target host type arch arch_path ba
|
|||
|
||||
if (NOT EXISTS "${prefix}")
|
||||
message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}")
|
||||
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.18")
|
||||
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.3.0")
|
||||
if (WIN32)
|
||||
set(aqt_path "${base_path}/aqt.exe")
|
||||
if (NOT EXISTS "${aqt_path}")
|
||||
|
|
|
|||
|
|
@ -1,49 +1,51 @@
|
|||
# Gets a UTC timstamp and sets the provided variable to it
|
||||
function(get_timestamp _var)
|
||||
string(TIMESTAMP timestamp UTC)
|
||||
set(${_var} "${timestamp}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
get_timestamp(BUILD_DATE)
|
||||
macro(generate_build_info)
|
||||
# Gets a UTC timstamp and sets the provided variable to it
|
||||
function(get_timestamp _var)
|
||||
string(TIMESTAMP timestamp UTC)
|
||||
set(${_var} "${timestamp}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
get_timestamp(BUILD_DATE)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${SRC_DIR}/externals/cmake-modules")
|
||||
list(APPEND CMAKE_MODULE_PATH "${SRC_DIR}/externals/cmake-modules")
|
||||
|
||||
if (EXISTS "${SRC_DIR}/.git/objects")
|
||||
# Find the package here with the known path so that the GetGit commands can find it as well
|
||||
find_package(Git QUIET PATHS "${GIT_EXECUTABLE}")
|
||||
if (EXISTS "${SRC_DIR}/.git/objects")
|
||||
# Find the package here with the known path so that the GetGit commands can find it as well
|
||||
find_package(Git QUIET PATHS "${GIT_EXECUTABLE}")
|
||||
|
||||
# only use Git to check revision info when source is obtained via Git
|
||||
include(GetGitRevisionDescription)
|
||||
get_git_head_revision(GIT_REF_SPEC GIT_REV)
|
||||
git_describe(GIT_DESC --always --long --dirty)
|
||||
git_branch_name(GIT_BRANCH)
|
||||
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
|
||||
# unified source archive
|
||||
file(READ "${SRC_DIR}/GIT-COMMIT" GIT_REV_RAW LIMIT 64)
|
||||
string(STRIP "${GIT_REV_RAW}" GIT_REV)
|
||||
string(SUBSTRING "${GIT_REV_RAW}" 0 9 GIT_DESC)
|
||||
set(GIT_BRANCH "HEAD")
|
||||
else()
|
||||
# self-packed archive?
|
||||
set(GIT_REV "UNKNOWN")
|
||||
set(GIT_DESC "UNKNOWN")
|
||||
set(GIT_BRANCH "UNKNOWN")
|
||||
endif()
|
||||
string(SUBSTRING "${GIT_REV}" 0 7 GIT_SHORT_REV)
|
||||
|
||||
# Set build version
|
||||
set(REPO_NAME "")
|
||||
set(BUILD_VERSION "0")
|
||||
set(BUILD_FULLNAME "${GIT_SHORT_REV}")
|
||||
if (DEFINED ENV{CI} AND DEFINED ENV{GITHUB_ACTIONS})
|
||||
if ($ENV{GITHUB_REF_TYPE} STREQUAL "tag")
|
||||
set(GIT_TAG $ENV{GITHUB_REF_NAME})
|
||||
# only use Git to check revision info when source is obtained via Git
|
||||
include(GetGitRevisionDescription)
|
||||
get_git_head_revision(GIT_REF_SPEC GIT_REV)
|
||||
git_describe(GIT_DESC --always --long --dirty)
|
||||
git_branch_name(GIT_BRANCH)
|
||||
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
|
||||
# unified source archive
|
||||
file(READ "${SRC_DIR}/GIT-COMMIT" GIT_REV_RAW LIMIT 64)
|
||||
string(STRIP "${GIT_REV_RAW}" GIT_REV)
|
||||
string(SUBSTRING "${GIT_REV_RAW}" 0 9 GIT_DESC)
|
||||
set(GIT_BRANCH "HEAD")
|
||||
else()
|
||||
# self-packed archive?
|
||||
set(GIT_REV "UNKNOWN")
|
||||
set(GIT_DESC "UNKNOWN")
|
||||
set(GIT_BRANCH "UNKNOWN")
|
||||
endif()
|
||||
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
|
||||
file(READ "${SRC_DIR}/GIT-TAG" GIT_TAG)
|
||||
string(STRIP ${GIT_TAG} GIT_TAG)
|
||||
endif()
|
||||
string(SUBSTRING "${GIT_REV}" 0 7 GIT_SHORT_REV)
|
||||
|
||||
if (DEFINED GIT_TAG AND NOT "${GIT_TAG}" STREQUAL "unknown")
|
||||
set(BUILD_VERSION "${GIT_TAG}")
|
||||
set(BUILD_FULLNAME "${BUILD_VERSION}")
|
||||
endif()
|
||||
# Set build version
|
||||
set(REPO_NAME "")
|
||||
set(BUILD_VERSION "0")
|
||||
set(BUILD_FULLNAME "${GIT_SHORT_REV}")
|
||||
if (DEFINED ENV{CI} AND DEFINED ENV{GITHUB_ACTIONS})
|
||||
if ($ENV{GITHUB_REF_TYPE} STREQUAL "tag")
|
||||
set(GIT_TAG $ENV{GITHUB_REF_NAME})
|
||||
endif()
|
||||
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
|
||||
file(READ "${SRC_DIR}/GIT-TAG" GIT_TAG)
|
||||
string(STRIP ${GIT_TAG} GIT_TAG)
|
||||
endif()
|
||||
|
||||
if (DEFINED GIT_TAG AND NOT "${GIT_TAG}" STREQUAL "unknown")
|
||||
set(BUILD_VERSION "${GIT_TAG}")
|
||||
set(BUILD_FULLNAME "${BUILD_VERSION}")
|
||||
endif()
|
||||
endmacro()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
list(APPEND CMAKE_MODULE_PATH "${SRC_DIR}/CMakeModules")
|
||||
include(GenerateBuildInfo)
|
||||
generate_build_info()
|
||||
|
||||
# The variable SRC_DIR must be passed into the script (since it uses the current build directory for all values of CMAKE_*_DIR)
|
||||
set(VIDEO_CORE "${SRC_DIR}/src/video_core")
|
||||
|
|
@ -10,6 +11,10 @@ set(HASH_FILES
|
|||
"${VIDEO_CORE}/renderer_opengl/gl_shader_util.h"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.cpp"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.h"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.cpp"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.h"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.cpp"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.h"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.cpp"
|
||||
|
|
@ -18,6 +23,7 @@ set(HASH_FILES
|
|||
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/generator/pica_fs_config.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/pica_fs_config.h"
|
||||
"${VIDEO_CORE}/shader/generator/profile.h"
|
||||
"${VIDEO_CORE}/shader/generator/shader_gen.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/generator/shader_uniforms.cpp"
|
||||
|
|
|
|||
49
README.md
49
README.md
|
|
@ -1,7 +1,9 @@
|
|||

|
||||
|
||||

|
||||
 <!--  -->
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<b>Azahar</b> is an open-source 3DS emulator project based on Citra.
|
||||
|
|
@ -14,36 +16,48 @@ The goal of this project is to be the de-facto platform for future development.
|
|||
|
||||
### Windows
|
||||
|
||||
Download the latest release from [Releases](https://github.com/azahar-emu/azahar/releases).
|
||||
Azahar is available as both an installer and a zip archive.
|
||||
|
||||
If you are unsure of whether you want to use MSYS2 or MSVC, use MSYS2.
|
||||
Download the latest release in your preferred format from the [Releases](https://github.com/azahar-emu/azahar/releases) page.
|
||||
|
||||
If you are unsure of whether you want to use MSVC or MSYS2, use MSYS2.
|
||||
|
||||
---
|
||||
|
||||
### MacOS
|
||||
|
||||
Download the latest release from [Releases](https://github.com/azahar-emu/azahar/releases).
|
||||
To download a build that will work on all Macs, you can download the `macos-universal` build on the [Releases](https://github.com/azahar-emu/azahar/releases) page.
|
||||
|
||||
The `macos-universal` download will work on both Intel and Apple Silicon Macs.
|
||||
Alternatively, if you wish to download a build specifically for your Mac, you can choose either:
|
||||
|
||||
- `macos-arm64` for Apple Silicon Macs
|
||||
- `macos-x86_64` for Intel Macs
|
||||
|
||||
---
|
||||
|
||||
### Android
|
||||
The recommended method of downloading Azahar on Android is via the Google Play store:
|
||||
|
||||
There are two variants of Azahar available on Android, those being the Vanilla and Google Play builds.
|
||||
|
||||
The Vanilla build is technically superior, as it uses an alternative method of file management which is faster, but isn't permitted on the Google Play store.
|
||||
|
||||
For most users, we currently recommended downloading Azahar on Android via the Google Play Store for ease of accessibility:
|
||||
|
||||
<a href='https://play.google.com/store/apps/details?id=io.github.lime3ds.android'><img width='180' alt='Get it on Google Play' src='https://raw.githubusercontent.com/pioug/google-play-badges/06ccd9252af1501613da2ca28eaffe31307a4e6d/svg/English.svg'/></a>
|
||||
|
||||
Alternatively, you can install the app using Obtainium:
|
||||
Alternatively, you can install the app using Obtainium, allowing you to use the Vanilla variant:
|
||||
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-emu/azahar` into the 'App Source URL' section
|
||||
4. Click 'Add'
|
||||
5. Click 'Install'
|
||||
5. Click 'Install', and select the preferred variant
|
||||
|
||||
If you wish, you can also simply install the latest APK from the [Releases](https://github.com/azahar-emu/azahar/releases) page.
|
||||
|
||||
Keep in mind that you will not recieve automatic updates when installing via the APK.
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
The recommended format for using Azahar on Linux is the Flatpak available on Flathub:
|
||||
|
|
@ -52,6 +66,13 @@ The recommended format for using Azahar on Linux is the Flatpak available on Fla
|
|||
|
||||
Azahar is also available as an AppImage on the [Releases](https://github.com/azahar-emu/azahar/releases) page.
|
||||
|
||||
There are two variants of the AppImage available, those being `azahar.AppImage` and `azahar-wayland.AppImage`.
|
||||
|
||||
If you are unsure of which variant to use, we recommend using the default `azahar.AppImage`. This is because of upstream issues in the Wayland ecosystem which may cause problems when running the emulator (e.g. [#1162](https://github.com/azahar-emu/azahar/issues/1162)).
|
||||
|
||||
Unless you explicitly require native Wayland support (e.g. you are running a system with no Xwayland), the non-Wayland variant is recommended.
|
||||
|
||||
The Flatpak build of Azahar also has native Wayland support disabled by default. If you require native Wayland support, it can be enabled using [Flatseal](https://flathub.org/en/apps/com.github.tchx84.Flatseal).
|
||||
|
||||
# Build instructions
|
||||
|
||||
|
|
@ -83,18 +104,23 @@ To do so, simply read https://github.com/azahar-emu/compatibility-list/blob/mast
|
|||
Contributing compatibility data helps more accurately reflect the current capabilities of the emulator, so it would be highly appreciated if you could go through the reporting process after completing a game.
|
||||
|
||||
# Minimum requirements
|
||||
|
||||
Below are the minimum requirements to run Azahar:
|
||||
|
||||
### Desktop
|
||||
|
||||
```
|
||||
Operating System: Windows 10 (64-bit), MacOS 13.4 (Ventura), or modern 64-bit Linux
|
||||
CPU: x86-64/ARM64 CPU (Windows for ARM not supported). Single core performance higher than 1,800 on Passmark
|
||||
CPU: x86-64/ARM64 CPU (Windows for ARM not supported).
|
||||
Single core performance higher than 1,800 on Passmark.
|
||||
SSE4.2 required on x86_64.
|
||||
GPU: OpenGL 4.3 or Vulkan 1.1 support
|
||||
Memory: 2GB of RAM. 4GB is recommended
|
||||
```
|
||||
### Android
|
||||
|
||||
```
|
||||
Operating System: Android 9.0+ (64-bit)
|
||||
Operating System: Android 10.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
|
||||
|
|
@ -107,6 +133,7 @@ We share public roadmaps for upcoming releases in the form of GitHub milestones.
|
|||
You can find these at https://github.com/azahar-emu/azahar/milestones.
|
||||
|
||||
# Join the conversation
|
||||
|
||||
We have a community Discord server where you can chat about the project, keep up to date with the latest announcements, or coordinate emulator development.
|
||||
|
||||
[](https://discord.gg/4ZjMpAp3M6)
|
||||
Join at https://discord.gg/4ZjMpAp3M6
|
||||
|
|
|
|||
4
dist/apple/Info.plist.in
vendored
4
dist/apple/Info.plist.in
vendored
|
|
@ -35,6 +35,7 @@
|
|||
<string>cci</string>
|
||||
<string>cxi</string>
|
||||
<string>cia</string>
|
||||
<string>3ds</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Nintendo 3DS File</string>
|
||||
|
|
@ -75,6 +76,9 @@
|
|||
<true/>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>True</string>
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<true/> <!-- Remove when Qt Liquid Glass issues are fixed upstream:
|
||||
https://bugreports.qt.io/browse/QTBUG-138942 -->
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
|
|
|||
2
dist/compatibility_list
vendored
2
dist/compatibility_list
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 31c12299126baf892b965defc6ba7810b9c42ccf
|
||||
Subproject commit eadcdfb84b6f3b95734e867d99fe16a9e8db717f
|
||||
1400
dist/languages/ca_ES_valencia.ts
vendored
1400
dist/languages/ca_ES_valencia.ts
vendored
File diff suppressed because it is too large
Load diff
1432
dist/languages/da_DK.ts
vendored
1432
dist/languages/da_DK.ts
vendored
File diff suppressed because it is too large
Load diff
1614
dist/languages/de.ts
vendored
1614
dist/languages/de.ts
vendored
File diff suppressed because it is too large
Load diff
1428
dist/languages/el.ts
vendored
1428
dist/languages/el.ts
vendored
File diff suppressed because it is too large
Load diff
1356
dist/languages/es_ES.ts
vendored
1356
dist/languages/es_ES.ts
vendored
File diff suppressed because it is too large
Load diff
1420
dist/languages/fi.ts
vendored
1420
dist/languages/fi.ts
vendored
File diff suppressed because it is too large
Load diff
1367
dist/languages/fr.ts
vendored
1367
dist/languages/fr.ts
vendored
File diff suppressed because it is too large
Load diff
1434
dist/languages/hu_HU.ts
vendored
1434
dist/languages/hu_HU.ts
vendored
File diff suppressed because it is too large
Load diff
1428
dist/languages/id.ts
vendored
1428
dist/languages/id.ts
vendored
File diff suppressed because it is too large
Load diff
1444
dist/languages/it.ts
vendored
1444
dist/languages/it.ts
vendored
File diff suppressed because it is too large
Load diff
1566
dist/languages/ja_JP.ts
vendored
1566
dist/languages/ja_JP.ts
vendored
File diff suppressed because it is too large
Load diff
1444
dist/languages/ko_KR.ts
vendored
1444
dist/languages/ko_KR.ts
vendored
File diff suppressed because it is too large
Load diff
1420
dist/languages/lt_LT.ts
vendored
1420
dist/languages/lt_LT.ts
vendored
File diff suppressed because it is too large
Load diff
1428
dist/languages/nb.ts
vendored
1428
dist/languages/nb.ts
vendored
File diff suppressed because it is too large
Load diff
1454
dist/languages/nl.ts
vendored
1454
dist/languages/nl.ts
vendored
File diff suppressed because it is too large
Load diff
1353
dist/languages/pl_PL.ts
vendored
1353
dist/languages/pl_PL.ts
vendored
File diff suppressed because it is too large
Load diff
1386
dist/languages/pt_BR.ts
vendored
1386
dist/languages/pt_BR.ts
vendored
File diff suppressed because it is too large
Load diff
1378
dist/languages/ro_RO.ts
vendored
1378
dist/languages/ro_RO.ts
vendored
File diff suppressed because it is too large
Load diff
1479
dist/languages/ru_RU.ts
vendored
1479
dist/languages/ru_RU.ts
vendored
File diff suppressed because it is too large
Load diff
1389
dist/languages/sv.ts
vendored
1389
dist/languages/sv.ts
vendored
File diff suppressed because it is too large
Load diff
1416
dist/languages/tr_TR.ts
vendored
1416
dist/languages/tr_TR.ts
vendored
File diff suppressed because it is too large
Load diff
1442
dist/languages/vi_VN.ts
vendored
1442
dist/languages/vi_VN.ts
vendored
File diff suppressed because it is too large
Load diff
1376
dist/languages/zh_CN.ts
vendored
1376
dist/languages/zh_CN.ts
vendored
File diff suppressed because it is too large
Load diff
1438
dist/languages/zh_TW.ts
vendored
1438
dist/languages/zh_TW.ts
vendored
File diff suppressed because it is too large
Load diff
1
dist/license.md
vendored
1
dist/license.md
vendored
|
|
@ -16,6 +16,7 @@ qt_themes/default/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com
|
|||
qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
|
||||
qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/default/icons/128x128/cartridge.png | CC0 1.0 | Designed by PabloMK7
|
||||
qt_themes/qdarkstyle/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com
|
||||
qt_themes/qdarkstyle/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com
|
||||
|
|
|
|||
1
dist/org.azahar_emu.Azahar.xml
vendored
1
dist/org.azahar_emu.Azahar.xml
vendored
|
|
@ -16,6 +16,7 @@
|
|||
<expanded-acronym>CTR Cart Image</expanded-acronym>
|
||||
<icon name="azahar"/>
|
||||
<glob pattern="*.cci"/>
|
||||
<glob pattern="*.3ds"/>
|
||||
<magic><match value="NCSD" type="string" offset="256"/></magic>
|
||||
</mime-type>
|
||||
|
||||
|
|
|
|||
BIN
dist/qt_themes/default/icons/128x128/cartridge.png
vendored
Normal file
BIN
dist/qt_themes/default/icons/128x128/cartridge.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
7
dist/qt_themes/default/icons/index.theme
vendored
7
dist/qt_themes/default/icons/index.theme
vendored
|
|
@ -1,13 +1,16 @@
|
|||
[Icon Theme]
|
||||
Name=default
|
||||
Comment=default theme
|
||||
Directories=16x16,48x48,256x256
|
||||
Directories=16x16,48x48,128x128,256x256
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
|
||||
[48x48]
|
||||
Size=48
|
||||
|
||||
|
||||
[128x128]
|
||||
Size=128
|
||||
|
||||
[256x256]
|
||||
Size=256
|
||||
BIN
dist/qt_themes/default/icons_light/128x128/cartridge.png
vendored
Normal file
BIN
dist/qt_themes/default/icons_light/128x128/cartridge.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
|
|
@ -1,13 +1,16 @@
|
|||
[Icon Theme]
|
||||
Name=default
|
||||
Comment=default theme
|
||||
Directories=16x16,48x48,256x256
|
||||
Directories=16x16,48x48,128x128,256x256
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
|
||||
[48x48]
|
||||
Size=48
|
||||
|
||||
|
||||
[128x128]
|
||||
Size=128
|
||||
|
||||
[256x256]
|
||||
Size=256
|
||||
2
dist/qt_themes/default/theme_default.qrc
vendored
2
dist/qt_themes/default/theme_default.qrc
vendored
|
|
@ -13,6 +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="128x128/cartridge.png">icons/128x128/cartridge.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>
|
||||
|
|
@ -31,6 +32,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="128x128/cartridge.png">icons_light/128x128/cartridge.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>
|
||||
|
|
|
|||
31
docker/azahar-room/Dockerfile
Normal file
31
docker/azahar-room/Dockerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# This Dockerfile assumes that it is being built from the project root directory, e.g.:
|
||||
# $ docker build -f docker/azahar-room/Dockerfile -t azahar-room .
|
||||
|
||||
# --- Builder ----------------
|
||||
FROM opensauce04/azahar-build-environment:latest AS builder
|
||||
|
||||
COPY . /var/azahar-src
|
||||
|
||||
RUN mkdir builddir && cd builddir && \
|
||||
cmake /var/azahar-src -G Ninja \
|
||||
-DENABLE_QT=OFF \
|
||||
-DENABLE_TESTS=OFF \
|
||||
-DENABLE_ROOM=ON \
|
||||
-DENABLE_ROOM_STANDALONE=ON \
|
||||
-DENABLE_OPENGL=OFF $( : "TODO: Can we disable these automatically when there's no frontend?") \
|
||||
-DENABLE_VULKAN=OFF \
|
||||
-DENABLE_SDL2=OFF \
|
||||
-DENABLE_LIBUSB=OFF \
|
||||
-DENABLE_CUBEB=OFF \
|
||||
-DENABLE_OPENAL=OFF && \
|
||||
ninja && \
|
||||
mv bin/Release/azahar-room /usr/local/bin/ && \
|
||||
cd .. && rm -rf builddir
|
||||
|
||||
# --- Final ------------------
|
||||
FROM debian:trixie AS final
|
||||
|
||||
RUN apt-get update && apt-get -y full-upgrade
|
||||
RUN apt-get install -y iputils-ping net-tools
|
||||
|
||||
COPY --from=builder /usr/local/bin/azahar-room /usr/local/bin/azahar-room
|
||||
112
externals/CMakeLists.txt
vendored
112
externals/CMakeLists.txt
vendored
|
|
@ -1,24 +1,37 @@
|
|||
# Definitions for all external bundled libraries
|
||||
|
||||
# Suppress warnings from external libraries
|
||||
if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||
if (MSVC)
|
||||
add_compile_options(/W0)
|
||||
else()
|
||||
add_compile_options(-w)
|
||||
endif()
|
||||
|
||||
function(target_disable_warnings target)
|
||||
if (MSVC)
|
||||
target_compile_options(${target} INTERFACE /W0)
|
||||
else()
|
||||
target_compile_options(${target} INTERFACE -w)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules)
|
||||
include(DownloadExternals)
|
||||
include(ExternalProject)
|
||||
|
||||
# Boost
|
||||
if (NOT USE_SYSTEM_BOOST)
|
||||
if (USE_SYSTEM_BOOST)
|
||||
unset(BOOST_ROOT CACHE)
|
||||
unset(Boost_INCLUDE_DIR CACHE)
|
||||
set(Boost_NO_SYSTEM_PATHS OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
message(STATUS "Including vendored Boost library")
|
||||
set(BOOST_ROOT "${CMAKE_SOURCE_DIR}/externals/boost" CACHE STRING "")
|
||||
set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/externals/boost" CACHE STRING "")
|
||||
set(Boost_NO_SYSTEM_PATHS ON CACHE BOOL "")
|
||||
add_library(boost INTERFACE)
|
||||
target_include_directories(boost SYSTEM INTERFACE ${Boost_INCLUDE_DIR})
|
||||
target_include_directories(boost INTERFACE ${Boost_INCLUDE_DIR})
|
||||
target_disable_warnings(boost)
|
||||
|
||||
# Boost::serialization
|
||||
file(GLOB boost_serialization_SRC "${CMAKE_SOURCE_DIR}/externals/boost/libs/serialization/src/*.cpp")
|
||||
|
|
@ -33,11 +46,7 @@ if (NOT USE_SYSTEM_BOOST)
|
|||
${CMAKE_SOURCE_DIR}/externals/boost/libs/iostreams/src/mapped_file.cpp
|
||||
)
|
||||
target_link_libraries(boost_iostreams PUBLIC boost)
|
||||
# Add additional boost libs here; remember to ALIAS them in the root CMakeLists!
|
||||
else()
|
||||
unset(BOOST_ROOT CACHE)
|
||||
unset(Boost_INCLUDE_DIR CACHE)
|
||||
set(Boost_NO_SYSTEM_PATHS OFF CACHE BOOL "" FORCE)
|
||||
# Add additional boost libs here; remember to ALIAS them in the root CMakeLists!
|
||||
endif()
|
||||
|
||||
# Catch2
|
||||
|
|
@ -73,6 +82,7 @@ endif()
|
|||
# dds-ktx
|
||||
add_library(dds-ktx INTERFACE)
|
||||
target_include_directories(dds-ktx INTERFACE ./dds-ktx)
|
||||
target_disable_warnings(dds-ktx)
|
||||
|
||||
# fmt and Xbyak need to be added before dynarmic
|
||||
# libfmt
|
||||
|
|
@ -137,7 +147,8 @@ endif()
|
|||
|
||||
# MicroProfile
|
||||
add_library(microprofile INTERFACE)
|
||||
target_include_directories(microprofile SYSTEM INTERFACE ./microprofile)
|
||||
target_include_directories(microprofile INTERFACE ./microprofile)
|
||||
target_disable_warnings(microprofile)
|
||||
if (ENABLE_MICROPROFILE)
|
||||
target_compile_definitions(microprofile INTERFACE MICROPROFILE_ENABLED=1)
|
||||
else()
|
||||
|
|
@ -146,10 +157,11 @@ endif()
|
|||
|
||||
# Nihstro
|
||||
add_library(nihstro-headers INTERFACE)
|
||||
target_include_directories(nihstro-headers SYSTEM INTERFACE ./nihstro/include)
|
||||
if (MSVC)
|
||||
# TODO: For some reason MSVC still applies this warning even with /W0 for externals.
|
||||
target_compile_options(nihstro-headers INTERFACE /wd4715)
|
||||
target_include_directories(nihstro-headers INTERFACE ./nihstro/include)
|
||||
target_disable_warnings(nihstro-headers)
|
||||
if (NOT MSVC)
|
||||
# TODO: For some reason MSYS2 still applied this warnin even with -w
|
||||
target_compile_options(nihstro-headers INTERFACE -Wno-invalid-specialization)
|
||||
endif()
|
||||
|
||||
# Open Source Archives
|
||||
|
|
@ -173,7 +185,8 @@ if (USE_SYSTEM_FFMPEG_HEADERS)
|
|||
endif()
|
||||
if (NOT FOUND_FFMPEG_HEADERS)
|
||||
message(STATUS "Using bundled ffmpeg headers.")
|
||||
target_include_directories(library-headers SYSTEM INTERFACE ./library-headers/ffmpeg/include)
|
||||
target_include_directories(library-headers INTERFACE ./library-headers/ffmpeg/include)
|
||||
target_disable_warnings(library-headers)
|
||||
endif()
|
||||
|
||||
# SoundTouch
|
||||
|
|
@ -230,13 +243,8 @@ else()
|
|||
)
|
||||
target_link_libraries(zstd_seekable PUBLIC libzstd_static)
|
||||
|
||||
target_link_libraries(libzstd_static INTERFACE zstd_seekable)
|
||||
|
||||
add_library(zstd ALIAS libzstd_static)
|
||||
|
||||
install(TARGETS zstd_seekable
|
||||
EXPORT zstdExports
|
||||
)
|
||||
add_library(zstd INTERFACE)
|
||||
target_link_libraries(zstd INTERFACE libzstd_static zstd_seekable)
|
||||
endif()
|
||||
|
||||
# ENet
|
||||
|
|
@ -294,7 +302,8 @@ if (USE_SYSTEM_JSON)
|
|||
# Citra uses "#include <json.hpp>" so we have to add this manually
|
||||
target_include_directories(json-headers SYSTEM INTERFACE "${NLOHMANN_PREFIX}/nlohmann")
|
||||
else()
|
||||
target_include_directories(json-headers SYSTEM INTERFACE ./json)
|
||||
target_include_directories(json-headers INTERFACE ./json)
|
||||
target_disable_warnings(json-headers)
|
||||
endif()
|
||||
|
||||
# OpenSSL
|
||||
|
|
@ -310,7 +319,8 @@ if (NOT OPENSSL_FOUND)
|
|||
set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "")
|
||||
set(OPENSSLDIR "/etc/ssl/")
|
||||
add_subdirectory(libressl EXCLUDE_FROM_ALL)
|
||||
target_include_directories(ssl SYSTEM INTERFACE ./libressl/include)
|
||||
target_include_directories(ssl INTERFACE ./libressl/include)
|
||||
target_disable_warnings(ssl)
|
||||
target_compile_definitions(ssl PRIVATE -DHAVE_INET_NTOP)
|
||||
get_directory_property(OPENSSL_LIBRARIES
|
||||
DIRECTORY libressl
|
||||
|
|
@ -327,17 +337,20 @@ if(USE_SYSTEM_CPP_HTTPLIB)
|
|||
get_target_property(HTTP_LIBS httplib::httplib INTERFACE_LINK_LIBRARIES)
|
||||
if(HTTP_LIBS)
|
||||
message(WARNING "Shared cpp-http (${HTTP_LIBS}) not supported. Falling back to bundled...")
|
||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||
target_include_directories(httplib INTERFACE ./httplib)
|
||||
target_disable_warnings(httplib)
|
||||
else()
|
||||
if(CppHttp_FOUND)
|
||||
target_link_libraries(httplib INTERFACE httplib::httplib)
|
||||
else()
|
||||
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
|
||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||
endif()
|
||||
target_include_directories(httplib INTERFACE ./httplib)
|
||||
target_disable_warnings(httplib)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||
target_include_directories(httplib INTERFACE ./httplib)
|
||||
target_disable_warnings(httplib)
|
||||
endif()
|
||||
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
|
||||
|
|
@ -354,7 +367,8 @@ if (ENABLE_WEB_SERVICE)
|
|||
target_link_libraries(cpp-jwt INTERFACE cpp-jwt::cpp-jwt)
|
||||
else()
|
||||
add_library(cpp-jwt INTERFACE)
|
||||
target_include_directories(cpp-jwt SYSTEM INTERFACE ./cpp-jwt/include)
|
||||
target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include)
|
||||
target_disable_warnings(cpp-jwt)
|
||||
target_compile_definitions(cpp-jwt INTERFACE CPP_JWT_USE_VENDORED_NLOHMANN_JSON)
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -402,6 +416,24 @@ endif()
|
|||
|
||||
# Vulkan dependencies
|
||||
if (ENABLE_VULKAN)
|
||||
# spirv-headers
|
||||
if(USE_SYSTEM_SPIRV_HEADERS)
|
||||
find_package(SPIRV-Headers REQUIRED)
|
||||
if(TARGET SPIRV-Headers::SPIRV-Headers)
|
||||
message(STATUS "Found SPIRV headers")
|
||||
get_target_property(SPIRV-Headers_SOURCE_DIR SPIRV-Headers::SPIRV-Headers INTERFACE_INCLUDE_DIRECTORIES)
|
||||
set(SPIRV-Headers_SOURCE_DIR "${SPIRV-Headers_SOURCE_DIR}/../") # Not sure why this is necessary
|
||||
endif()
|
||||
else()
|
||||
set(SPIRV-Headers_SOURCE_DIR "${CMAKE_SOURCE_DIR}/externals/spirv-headers")
|
||||
add_subdirectory(spirv-headers EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
# spirv-tools
|
||||
# TODO: Implement USE_SYSTEM_SPIRV_TOOLS -OS
|
||||
set(SPIRV_SKIP_EXECUTABLES ON)
|
||||
add_subdirectory(spirv-tools EXCLUDE_FROM_ALL)
|
||||
|
||||
# glslang
|
||||
if(USE_SYSTEM_GLSLANG)
|
||||
find_package(glslang REQUIRED)
|
||||
|
|
@ -413,10 +445,6 @@ if (ENABLE_VULKAN)
|
|||
get_target_property(GLSLANG_PREFIX glslang::SPIRV INTERFACE_INCLUDE_DIRECTORIES)
|
||||
target_include_directories(SPIRV SYSTEM INTERFACE "${GLSLANG_PREFIX}/glslang")
|
||||
else()
|
||||
set(SPIRV-Headers_SOURCE_DIR "${CMAKE_SOURCE_DIR}/externals/spirv-headers")
|
||||
add_subdirectory(spirv-headers EXCLUDE_FROM_ALL)
|
||||
set(SPIRV_SKIP_EXECUTABLES ON)
|
||||
add_subdirectory(spirv-tools EXCLUDE_FROM_ALL)
|
||||
set(SKIP_GLSLANG_INSTALL ON CACHE BOOL "")
|
||||
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "")
|
||||
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "")
|
||||
|
|
@ -439,7 +467,8 @@ if (ENABLE_VULKAN)
|
|||
endif()
|
||||
else()
|
||||
add_library(vma INTERFACE)
|
||||
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
|
||||
target_include_directories(vma INTERFACE ./vma/include)
|
||||
target_disable_warnings(vma)
|
||||
endif()
|
||||
|
||||
# vulkan-headers
|
||||
|
|
@ -451,7 +480,8 @@ if (ENABLE_VULKAN)
|
|||
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
|
||||
endif()
|
||||
else()
|
||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||
target_include_directories(vulkan-headers INTERFACE ./vulkan-headers/include)
|
||||
target_disable_warnings(vulkan-headers)
|
||||
endif()
|
||||
|
||||
# adrenotools
|
||||
|
|
@ -459,3 +489,17 @@ if (ENABLE_VULKAN)
|
|||
add_subdirectory(libadrenotools)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(XXHASH_BUILD_XXHSUM OFF)
|
||||
add_subdirectory(xxHash/cmake_unofficial EXCLUDE_FROM_ALL)
|
||||
target_compile_definitions(xxhash PRIVATE XXH_FORCE_MEMORY_ACCESS=2)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
target_compile_definitions(xxhash PRIVATE XXH_VECTOR=XXH_SSE2)
|
||||
message(STATUS "Enabling SSE2 for xxHash")
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64|armv8")
|
||||
target_compile_definitions(xxhash PRIVATE XXH_VECTOR=XXH_NEON)
|
||||
message(STATUS "Enabling NEON for xxHash")
|
||||
else()
|
||||
target_compile_definitions(xxhash PRIVATE XXH_VECTOR=XXH_SCALAR)
|
||||
message(STATUS "Disabling SIMD for xxHash")
|
||||
endif()
|
||||
2
externals/boost
vendored
2
externals/boost
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 3c27c785ad0f8a742af02e620dc225673f3a12d8
|
||||
Subproject commit 2c82bd787302398bcae990e3c9ab2b451284f4ca
|
||||
|
|
@ -25,6 +25,7 @@ option(USE_SYSTEM_LODEPNG "Use the system lodepng (instead of the bundled one)"
|
|||
option(USE_SYSTEM_OPENAL "Use the system OpenAL (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_VMA "Use the system VulkanMemoryAllocator (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_VULKAN_HEADERS "Use the system Vulkan headers (instead of the bundled ones)" OFF)
|
||||
option(USE_SYSTEM_SPIRV_HEADERS "Use the system SPIRV headers (instead of the bundled ones)" OFF)
|
||||
option(USE_SYSTEM_CATCH2 "Use the system Catch2 (instead of the bundled one)" OFF)
|
||||
|
||||
# Qt and MoltenVK are handled separately
|
||||
|
|
@ -50,6 +51,7 @@ CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_LODEPNG "Disable system lodepng" OFF "USE_
|
|||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_OPENAL "Disable system OpenAL" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VMA "Disable system VulkanMemoryAllocator" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VULKAN_HEADERS "Disable system Vulkan headers" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SPIRV_HEADERS "Disable system SPIRV headers" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CATCH2 "Disable system Catch2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
|
||||
set(LIB_VAR_LIST
|
||||
|
|
@ -75,6 +77,7 @@ set(LIB_VAR_LIST
|
|||
OPENAL
|
||||
VMA
|
||||
VULKAN_HEADERS
|
||||
SPIRV_HEADERS
|
||||
CATCH2
|
||||
)
|
||||
|
||||
|
|
|
|||
2
externals/dynarmic
vendored
2
externals/dynarmic
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 278405bd71999ed3f3c77c5f78344a06fef798b9
|
||||
Subproject commit 526227eebe1efff3fb14dbf494b9c5b44c2e9c1f
|
||||
2
externals/fmt
vendored
2
externals/fmt
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 123913715afeb8a437e6388b4473fcc4753e1c9a
|
||||
Subproject commit e424e3f2e607da02742f73db84873b8084fc714c
|
||||
2
externals/sdl2/SDL
vendored
2
externals/sdl2/SDL
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 2359383fc187386204c3bb22de89655a494cd128
|
||||
Subproject commit 5d249570393f7a37e037abf22cd6012a4cc56a71
|
||||
2
externals/teakra
vendored
2
externals/teakra
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 01db7cdd00aabcce559a8dddce8798dabb71949b
|
||||
Subproject commit 3d697a18df504f4677b65129d9ab14c7c597e3eb
|
||||
1
externals/xxHash
vendored
Submodule
1
externals/xxHash
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit e626a72bc2321cd320e953a0ccf1584cad60f363
|
||||
|
|
@ -357,3 +357,4 @@ plus.png (Default, Dark) | CC0 1.0 | Designed by BreadFish64 fro
|
|||
plus.png (Colorful, Colorful Dark) | CC BY-ND 3.0 | https://icons8.com
|
||||
sd_card.png | CC BY-ND 3.0 | https://icons8.com
|
||||
star.png | CC BY-ND 3.0 | https://icons8.com
|
||||
cartridge.png | CC0 1.0 | Designed by PabloMK7
|
||||
|
|
|
|||
|
|
@ -174,6 +174,9 @@ endif()
|
|||
if(ENABLE_VULKAN)
|
||||
add_compile_definitions(ENABLE_VULKAN)
|
||||
endif()
|
||||
if(ENABLE_DEVELOPER_OPTIONS)
|
||||
add_compile_definitions(ENABLE_DEVELOPER_OPTIONS)
|
||||
endif()
|
||||
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(core)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ android {
|
|||
// The application ID refers to Lime3DS to allow for
|
||||
// the Play Store listing, which was originally set up for Lime3DS, to still be used.
|
||||
applicationId = "io.github.lime3ds.android"
|
||||
minSdk = 28
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = autoVersion
|
||||
versionName = getGitVersion()
|
||||
|
|
@ -125,7 +125,7 @@ android {
|
|||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isShrinkResources = true
|
||||
isShrinkResources = true // TODO: Does this actually do anything when isDebuggable is enabled? -OS
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
proguardFiles(
|
||||
|
|
@ -135,6 +135,22 @@ android {
|
|||
isDefault = true
|
||||
}
|
||||
|
||||
// Same as above, but with isDebuggable disabled.
|
||||
// Primarily exists to allow development on hardened_malloc systems (e.g. GrapheneOS) without constantly tripping over years-old and seemingly harmless memory bugs.
|
||||
// We should fix those bugs eventually, but for now this exists as a workaround to allow other work to be done.
|
||||
register("relWithDebInfoLite") {
|
||||
initWith(getByName("relWithDebInfo"))
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = false
|
||||
installation {
|
||||
enableBaselineProfile = false // Disabled by default when isDebuggable is true
|
||||
}
|
||||
lint {
|
||||
checkReleaseBuilds = false // Ditto
|
||||
// The name of this property is misleading, this doesn't actually disable linting for the `release` build.
|
||||
}
|
||||
}
|
||||
|
||||
// Signed by debug key disallowing distribution on Play Store.
|
||||
// Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
|
||||
debug {
|
||||
|
|
@ -148,6 +164,18 @@ android {
|
|||
|
||||
flavorDimensions.add("version")
|
||||
|
||||
productFlavors {
|
||||
register("vanilla") {
|
||||
isDefault = true
|
||||
dimension = "version"
|
||||
versionNameSuffix = "-vanilla"
|
||||
}
|
||||
register("googlePlay") {
|
||||
dimension = "version"
|
||||
versionNameSuffix = "-googleplay"
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version = "3.25.0+"
|
||||
|
|
@ -186,7 +214,7 @@ dependencies {
|
|||
|
||||
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
||||
val downloadVulkanValidationLayers = tasks.register<Download>("downloadVulkanValidationLayers") {
|
||||
src("https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.4.304.1/android-binaries-1.4.304.1.zip")
|
||||
src("https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.4.313.0/android-binaries-1.4.313.0.zip")
|
||||
dest(file("${layout.buildDirectory.get().asFile.path}/tmp/Vulkan-ValidationLayers.zip"))
|
||||
onlyIfModified(true)
|
||||
}
|
||||
|
|
|
|||
8
src/android/app/src/googlePlay/AndroidManifest.xml
Normal file
8
src/android/app/src/googlePlay/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- These permissions aren't allowed by Google. We asked, and they declined. -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||
|
||||
</manifest>
|
||||
|
|
@ -29,6 +29,8 @@
|
|||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name="org.citra.citra_emu.CitraApplication"
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ package org.citra.citra_emu
|
|||
import android.Manifest.permission
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.Surface
|
||||
|
|
@ -18,11 +20,16 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.activities.EmulationActivity
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.RemovableStorageHelper
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Date
|
||||
|
||||
|
|
@ -96,6 +103,24 @@ object NativeLibrary {
|
|||
*/
|
||||
external fun onTouchMoved(xAxis: Float, yAxis: Float)
|
||||
|
||||
/**
|
||||
* Handles touch events on the secondary display.
|
||||
*
|
||||
* @param xAxis The value of the x-axis.
|
||||
* @param yAxis The value of the y-axis.
|
||||
* @param pressed To identify if the touch held down or released.
|
||||
* @return true if the pointer is within the touchscreen
|
||||
*/
|
||||
external fun onSecondaryTouchEvent(xAxis: Float, yAxis: Float, pressed: Boolean): Boolean
|
||||
|
||||
/**
|
||||
* Handles touch movement on the secondary display.
|
||||
*
|
||||
* @param xAxis The value of the instantaneous x-axis.
|
||||
* @param yAxis The value of the instantaneous y-axis.
|
||||
*/
|
||||
external fun onSecondaryTouchMoved(xAxis: Float, yAxis: Float)
|
||||
|
||||
external fun reloadSettings()
|
||||
|
||||
external fun getTitleId(filename: String): Long
|
||||
|
|
@ -114,6 +139,12 @@ object NativeLibrary {
|
|||
external fun createLogFile()
|
||||
external fun logUserDirectory(directory: String)
|
||||
|
||||
/**
|
||||
* Set the inserted cartridge that will appear
|
||||
* in the home menu. Empty string to clear.
|
||||
*/
|
||||
external fun setInsertedCartridge(path: String)
|
||||
|
||||
/**
|
||||
* Begins emulation.
|
||||
*/
|
||||
|
|
@ -124,6 +155,10 @@ object NativeLibrary {
|
|||
external fun surfaceDestroyed()
|
||||
external fun doFrame()
|
||||
|
||||
// Second window
|
||||
external fun secondarySurfaceChanged(secondary_surface: Surface)
|
||||
external fun secondarySurfaceDestroyed()
|
||||
|
||||
/**
|
||||
* Unpauses emulation from a paused state.
|
||||
*/
|
||||
|
|
@ -245,6 +280,12 @@ object NativeLibrary {
|
|||
canContinue = false
|
||||
}
|
||||
|
||||
CoreError.ErrorN3DSApplication -> {
|
||||
title = emulationActivity.getString(R.string.invalid_system_mode)
|
||||
message = emulationActivity.getString(R.string.invalid_system_mode_message)
|
||||
canContinue = false
|
||||
}
|
||||
|
||||
CoreError.ErrorUnknown -> {
|
||||
title = emulationActivity.getString(R.string.fatal_error)
|
||||
message = emulationActivity.getString(R.string.fatal_error_message)
|
||||
|
|
@ -567,6 +608,47 @@ object NativeLibrary {
|
|||
*/
|
||||
external fun logDeviceInfo()
|
||||
|
||||
enum class CompressStatus(val value: Int) {
|
||||
SUCCESS(0),
|
||||
COMPRESS_UNSUPPORTED(1),
|
||||
COMPRESS_ALREADY_COMPRESSED(2),
|
||||
COMPRESS_FAILED(3),
|
||||
DECOMPRESS_UNSUPPORTED(4),
|
||||
DECOMPRESS_NOT_COMPRESSED(5),
|
||||
DECOMPRESS_FAILED(6),
|
||||
INSTALLED_APPLICATION(7);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): CompressStatus =
|
||||
CompressStatus.entries.first { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
// Compression / Decompression
|
||||
private external fun compressFileNative(inputPath: String?, outputPath: String): Int
|
||||
|
||||
fun compressFile(inputPath: String?, outputPath: String): CompressStatus {
|
||||
return CompressStatus.fromValue(
|
||||
compressFileNative(inputPath, outputPath)
|
||||
)
|
||||
}
|
||||
|
||||
private external fun decompressFileNative(inputPath: String?, outputPath: String): Int
|
||||
|
||||
fun decompressFile(inputPath: String?, outputPath: String): CompressStatus {
|
||||
return CompressStatus.fromValue(
|
||||
decompressFileNative(inputPath, outputPath)
|
||||
)
|
||||
}
|
||||
|
||||
external fun getRecommendedExtension(inputPath: String?, shouldCompress: Boolean): String
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun onCompressProgress(total: Long, current: Long) {
|
||||
CompressProgressDialogViewModel.update(total, current)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createFile(directory: String, filename: String): Boolean =
|
||||
|
|
@ -607,6 +689,38 @@ object NativeLibrary {
|
|||
FileUtil.getFilesName(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getUserDirectory(uriOverride: Uri? = null): String {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
|
||||
val preferences: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
|
||||
val dirSep = "/"
|
||||
val udUri = uriOverride ?:
|
||||
preferences.getString("CITRA_DIRECTORY", "")!!.toUri()
|
||||
val udPathSegment = udUri.lastPathSegment!!
|
||||
val udVirtualPath = udPathSegment.substringAfter(":")
|
||||
|
||||
if (udPathSegment.startsWith("primary:")) { // User directory is located in primary storage
|
||||
val primaryStoragePath = Environment.getExternalStorageDirectory().absolutePath
|
||||
return primaryStoragePath + dirSep + udVirtualPath + dirSep
|
||||
} else { // User directory probably located on a removable storage device
|
||||
val storageIdString = udPathSegment.substringBefore(":")
|
||||
val udRemovablePath = RemovableStorageHelper.getRemovableStoragePath(storageIdString)
|
||||
|
||||
if (udRemovablePath == null) {
|
||||
android.util.Log.e("NativeLibrary",
|
||||
"Unknown mount location for storage device '$storageIdString' (URI: $udUri)"
|
||||
)
|
||||
return ""
|
||||
}
|
||||
return udRemovablePath + dirSep + udVirtualPath + dirSep
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getSize(path: String): Long =
|
||||
|
|
@ -616,6 +730,10 @@ object NativeLibrary {
|
|||
FileUtil.getFileSize(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getBuildFlavor(): String = BuildConfig.FLAVOR
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun fileExists(path: String): Boolean =
|
||||
|
|
@ -667,6 +785,24 @@ object NativeLibrary {
|
|||
FileUtil.renameFile(path, destinationFilename)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean =
|
||||
CitraApplication.documentsTree.updateDocumentLocation(sourcePath, destinationPath)
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun moveFile(filename: String, sourceDirPath: String, destinationDirPath: String): Boolean =
|
||||
if (FileUtil.isNativePath(sourceDirPath)) {
|
||||
try {
|
||||
CitraApplication.documentsTree.moveFile(filename, sourceDirPath, destinationDirPath)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
FileUtil.moveFile(filename, sourceDirPath, destinationDirPath)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun deleteDocument(path: String): Boolean =
|
||||
|
|
@ -680,6 +816,7 @@ object NativeLibrary {
|
|||
ErrorSystemFiles,
|
||||
ErrorSavestate,
|
||||
ErrorArticDisconnected,
|
||||
ErrorN3DSApplication,
|
||||
ErrorUnknown
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import android.content.Intent
|
|||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
|
|
@ -33,6 +34,7 @@ import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
|
|||
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||
import org.citra.citra_emu.databinding.ActivityEmulationBinding
|
||||
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||
import org.citra.citra_emu.display.SecondaryDisplay
|
||||
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
|
|
@ -46,6 +48,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.Log
|
||||
import org.citra.citra_emu.utils.RefreshRateUtil
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.viewmodel.EmulationViewModel
|
||||
|
||||
|
|
@ -59,6 +62,15 @@ class EmulationActivity : AppCompatActivity() {
|
|||
private lateinit var binding: ActivityEmulationBinding
|
||||
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
|
||||
private lateinit var hotkeyUtility: HotkeyUtility
|
||||
private lateinit var secondaryDisplay: SecondaryDisplay
|
||||
|
||||
private val onShutdown = Runnable {
|
||||
if (intent.getBooleanExtra("launched_from_shortcut", false)) {
|
||||
finishAffinity()
|
||||
} else {
|
||||
this.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private val emulationFragment: EmulationFragment
|
||||
get() {
|
||||
|
|
@ -72,11 +84,13 @@ class EmulationActivity : AppCompatActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
RefreshRateUtil.enforceRefreshRate(this, sixtyHz = true)
|
||||
|
||||
ThemeUtil.setTheme(this)
|
||||
|
||||
settingsViewModel.settings.loadSettings()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
secondaryDisplay = SecondaryDisplay(this)
|
||||
secondaryDisplay.updateDisplay()
|
||||
|
||||
binding = ActivityEmulationBinding.inflate(layoutInflater)
|
||||
screenAdjustmentUtil = ScreenAdjustmentUtil(this, windowManager, settingsViewModel.settings)
|
||||
|
|
@ -99,13 +113,7 @@ class EmulationActivity : AppCompatActivity() {
|
|||
windowManager.defaultDisplay.rotation
|
||||
)
|
||||
|
||||
EmulationLifecycleUtil.addShutdownHook(hook = {
|
||||
if (intent.getBooleanExtra("launched_from_shortcut", false)) {
|
||||
finishAffinity()
|
||||
} else {
|
||||
this.finish()
|
||||
}
|
||||
})
|
||||
EmulationLifecycleUtil.addShutdownHook(onShutdown)
|
||||
|
||||
isEmulationRunning = true
|
||||
instance = this
|
||||
|
|
@ -136,6 +144,11 @@ class EmulationActivity : AppCompatActivity() {
|
|||
applyOrientationSettings() // Check for orientation settings changes on runtime
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
secondaryDisplay.releasePresentation()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
enableFullscreenImmersive()
|
||||
|
|
@ -143,6 +156,7 @@ class EmulationActivity : AppCompatActivity() {
|
|||
|
||||
public override fun onRestart() {
|
||||
super.onRestart()
|
||||
secondaryDisplay.updateDisplay()
|
||||
NativeLibrary.reloadCameraDevices()
|
||||
}
|
||||
|
||||
|
|
@ -157,10 +171,13 @@ class EmulationActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
EmulationLifecycleUtil.clear()
|
||||
EmulationLifecycleUtil.removeHook(onShutdown)
|
||||
NativeLibrary.playTimeManagerStop()
|
||||
isEmulationRunning = false
|
||||
instance = null
|
||||
secondaryDisplay.releasePresentation()
|
||||
secondaryDisplay.releaseVD()
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
@ -325,6 +342,7 @@ class EmulationActivity : AppCompatActivity() {
|
|||
preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1)
|
||||
val guestOrientation =
|
||||
preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1)
|
||||
val inverted = preferences.getBoolean(InputBindingSetting.getInputAxisInvertedKey(axis),false);
|
||||
if (nextMapping == -1 || guestOrientation == -1) {
|
||||
// Axis is unmapped
|
||||
continue
|
||||
|
|
@ -333,6 +351,8 @@ class EmulationActivity : AppCompatActivity() {
|
|||
// Skip joystick wobble
|
||||
value = 0f
|
||||
}
|
||||
if (inverted) value = -value;
|
||||
|
||||
when (nextMapping) {
|
||||
NativeLibrary.ButtonType.STICK_LEFT -> {
|
||||
axisValuesCirclePad[guestOrientation] = value
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.widget.TextView
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
|
|
@ -57,13 +58,21 @@ import org.citra.citra_emu.utils.FileUtil
|
|||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity, private val inflater: LayoutInflater, private val openImageLauncher: ActivityResultLauncher<String>?) :
|
||||
class GameAdapter(
|
||||
private val activity: AppCompatActivity,
|
||||
private val inflater: LayoutInflater,
|
||||
private val openImageLauncher: ActivityResultLauncher<String>?,
|
||||
private val onRequestCompressOrDecompress: ((inputPath: String, suggestedName: String, shouldCompress: Boolean) -> Unit)? = null
|
||||
) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
private var lastClickTime = 0L
|
||||
private var imagePath: String? = null
|
||||
private var dialogShortcutBinding: DialogShortcutBinding? = null
|
||||
|
||||
private val preferences: SharedPreferences
|
||||
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
|
||||
fun handleShortcutImageResult(uri: Uri?) {
|
||||
val path = uri?.toString()
|
||||
if (path != null) {
|
||||
|
|
@ -191,6 +200,11 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||
binding.textGameTitle.text = game.title
|
||||
binding.textCompany.text = game.company
|
||||
binding.textGameRegion.text = game.regions
|
||||
binding.imageCartridge.visibility = if (preferences.getString("insertedCartridge", "") != game.path) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
val backgroundColorId =
|
||||
if (
|
||||
|
|
@ -340,12 +354,29 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||
val bottomSheetDialog = BottomSheetDialog(context)
|
||||
bottomSheetDialog.setContentView(bottomSheetView)
|
||||
|
||||
val insertable = game.isInsertable
|
||||
val inserted = insertable && (preferences.getString("insertedCartridge", "") == game.path)
|
||||
|
||||
bottomSheetView.findViewById<TextView>(R.id.about_game_title).text = game.title
|
||||
bottomSheetView.findViewById<TextView>(R.id.about_game_company).text = game.company
|
||||
bottomSheetView.findViewById<TextView>(R.id.about_game_region).text = game.regions
|
||||
bottomSheetView.findViewById<TextView>(R.id.about_game_id).text = context.getString(R.string.game_context_id) + " " + String.format("%016X", game.titleId)
|
||||
bottomSheetView.findViewById<TextView>(R.id.about_game_filename).text = context.getString(R.string.game_context_file) + " " + game.filename
|
||||
bottomSheetView.findViewById<TextView>(R.id.about_game_filetype).text = context.getString(R.string.game_context_type) + " " + game.fileType
|
||||
|
||||
val insertButton = bottomSheetView.findViewById<MaterialButton>(R.id.insert_cartridge_button)
|
||||
insertButton.text = if (inserted) { context.getString(R.string.game_context_eject) } else { context.getString(R.string.game_context_insert) }
|
||||
insertButton.visibility = if (insertable) View.VISIBLE else View.GONE
|
||||
insertButton.setOnClickListener {
|
||||
if (inserted) {
|
||||
preferences.edit().putString("insertedCartridge", "").apply()
|
||||
} else {
|
||||
preferences.edit().putString("insertedCartridge", game.path).apply()
|
||||
}
|
||||
bottomSheetDialog.dismiss()
|
||||
notifyItemRangeChanged(0, currentList.size)
|
||||
}
|
||||
|
||||
GameIconUtils.loadGameIcon(activity, game, bottomSheetView.findViewById(R.id.game_icon))
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.about_game_play).setOnClickListener {
|
||||
|
|
@ -441,6 +472,27 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
|||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
val compressDecompressButton = bottomSheetView.findViewById<MaterialButton>(R.id.compress_decompress)
|
||||
if (game.isInstalled) {
|
||||
compressDecompressButton.setOnClickListener {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.compress_decompress_installed_app),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
compressDecompressButton.alpha = 0.38f
|
||||
} else {
|
||||
compressDecompressButton.setOnClickListener {
|
||||
val shouldCompress = !game.isCompressed
|
||||
val recommendedExt = NativeLibrary.getRecommendedExtension(holder.game.path, shouldCompress)
|
||||
val baseName = holder.game.filename.substringBeforeLast('.')
|
||||
onRequestCompressOrDecompress?.invoke(holder.game.path, "$baseName.$recommendedExt", shouldCompress)
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
}
|
||||
compressDecompressButton.text = context.getString(if (!game.isCompressed) R.string.compress else R.string.decompress)
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.menu_button_open).setOnClickListener {
|
||||
showOpenContextMenu(it, game)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,4 +49,51 @@ enum class PortraitScreenLayout(val int: Int) {
|
|||
return entries.firstOrNull { it.int == int } ?: TOP_FULL_WIDTH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SecondaryDisplayLayout(val int: Int) {
|
||||
// These must match what is defined in src/common/settings.h
|
||||
NONE(0),
|
||||
TOP_SCREEN(1),
|
||||
BOTTOM_SCREEN(2),
|
||||
SIDE_BY_SIDE(3);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): SecondaryDisplayLayout {
|
||||
return entries.firstOrNull { it.int == int } ?: NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class StereoWhichDisplay(val int: Int) {
|
||||
// These must match what is defined in src/common/settings.h
|
||||
|
||||
NONE(0), // equivalent to StereoRenderOption = Off
|
||||
BOTH(1),
|
||||
PRIMARY_ONLY(2),
|
||||
SECONDARY_ONLY(3);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): StereoWhichDisplay {
|
||||
return entries.firstOrNull { it.int == int } ?: NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class StereoMode(val int: Int) {
|
||||
// These must match what is defined in src/common/settings.h
|
||||
|
||||
OFF(0),
|
||||
SIDE_BY_SIDE(1),
|
||||
SIDE_BY_SIDE_FULL(2),
|
||||
ANAGLYPH(3),
|
||||
INTERLACED(4),
|
||||
REVERSE_INTERLACED (5),
|
||||
CARDBOARD_VR (6);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): StereoMode {
|
||||
return entries.firstOrNull { it.int == int } ?: OFF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
// 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.display
|
||||
|
||||
import android.app.Presentation
|
||||
import android.content.Context
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
import android.os.Bundle
|
||||
import android.view.Display
|
||||
import android.view.MotionEvent
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.WindowManager
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
import org.citra.citra_emu.display.SecondaryDisplayLayout
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
|
||||
class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
|
||||
private var pres: SecondaryDisplayPresentation? = null
|
||||
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
private val vd: VirtualDisplay
|
||||
|
||||
init {
|
||||
vd = displayManager.createVirtualDisplay(
|
||||
"HiddenDisplay",
|
||||
1920,
|
||||
1080,
|
||||
320,
|
||||
null,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION
|
||||
)
|
||||
displayManager.registerDisplayListener(this, null)
|
||||
}
|
||||
|
||||
fun updateSurface() {
|
||||
NativeLibrary.secondarySurfaceChanged(pres!!.getSurfaceHolder().surface)
|
||||
}
|
||||
|
||||
fun destroySurface() {
|
||||
NativeLibrary.secondarySurfaceDestroyed()
|
||||
}
|
||||
|
||||
private fun getExternalDisplay(context: Context): Display? {
|
||||
val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
val currentDisplayId = context.display.displayId
|
||||
val displays = dm.displays
|
||||
val presDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
|
||||
val extDisplays = displays.filter {
|
||||
val isPresentable = presDisplays.any { pd -> pd.displayId == it.displayId }
|
||||
val isNotDefaultOrPresentable = it.displayId != Display.DEFAULT_DISPLAY || isPresentable
|
||||
isNotDefaultOrPresentable &&
|
||||
it.displayId != currentDisplayId &&
|
||||
it.name != "HiddenDisplay" &&
|
||||
it.state != Display.STATE_OFF &&
|
||||
it.isValid
|
||||
}
|
||||
// if there is a display called Built-In Display or Built-In Screen, prioritize the OTHER screen
|
||||
val selected = extDisplays.firstOrNull { ! it.name.contains("Built",true) }
|
||||
?: extDisplays.firstOrNull()
|
||||
return selected
|
||||
}
|
||||
|
||||
fun updateDisplay() {
|
||||
// decide if we are going to the external display or the internal one
|
||||
var display = getExternalDisplay(context)
|
||||
if (display == null ||
|
||||
IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) {
|
||||
display = vd.display
|
||||
}
|
||||
|
||||
// if our presentation is already on the right display, ignore
|
||||
if (pres?.display == display) return
|
||||
|
||||
// otherwise, make a new presentation
|
||||
releasePresentation()
|
||||
pres = SecondaryDisplayPresentation(context, display!!, this)
|
||||
pres?.show()
|
||||
}
|
||||
|
||||
fun releasePresentation() {
|
||||
pres?.dismiss()
|
||||
pres = null
|
||||
}
|
||||
|
||||
fun releaseVD() {
|
||||
displayManager.unregisterDisplayListener(this)
|
||||
vd.release()
|
||||
}
|
||||
|
||||
override fun onDisplayAdded(displayId: Int) {
|
||||
updateDisplay()
|
||||
}
|
||||
|
||||
override fun onDisplayRemoved(displayId: Int) {
|
||||
updateDisplay()
|
||||
}
|
||||
override fun onDisplayChanged(displayId: Int) {
|
||||
updateDisplay()
|
||||
}
|
||||
}
|
||||
class SecondaryDisplayPresentation(
|
||||
context: Context, display: Display, val parent: SecondaryDisplay
|
||||
) : Presentation(context, display) {
|
||||
private lateinit var surfaceView: SurfaceView
|
||||
private var touchscreenPointerId = -1
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
window?.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
)
|
||||
|
||||
// Initialize SurfaceView
|
||||
surfaceView = SurfaceView(context)
|
||||
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
|
||||
}
|
||||
|
||||
override fun surfaceChanged(
|
||||
holder: SurfaceHolder, format: Int, width: Int, height: Int
|
||||
) {
|
||||
parent.updateSurface()
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
parent.destroySurface()
|
||||
}
|
||||
})
|
||||
|
||||
this.surfaceView.setOnTouchListener { _, event ->
|
||||
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
if (touchscreenPointerId == -1) {
|
||||
touchscreenPointerId = pointerId
|
||||
NativeLibrary.onSecondaryTouchEvent(
|
||||
event.getX(pointerIndex),
|
||||
event.getY(pointerIndex),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val index = event.findPointerIndex(touchscreenPointerId)
|
||||
if (index != -1) {
|
||||
NativeLibrary.onSecondaryTouchMoved(
|
||||
event.getX(index),
|
||||
event.getY(index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
if (pointerId == touchscreenPointerId) {
|
||||
NativeLibrary.onSecondaryTouchEvent(0f, 0f, false)
|
||||
touchscreenPointerId = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
setContentView(surfaceView) // Set SurfaceView as content
|
||||
}
|
||||
|
||||
// Publicly accessible method to get the SurfaceHolder
|
||||
fun getSurfaceHolder(): SurfaceHolder {
|
||||
return surfaceView.holder
|
||||
}
|
||||
}
|
||||
|
|
@ -19,13 +19,15 @@ enum class BooleanSetting(
|
|||
INSTANT_DEBUG_LOG("instant_debug_log", Settings.SECTION_DEBUG, false),
|
||||
ENABLE_RPC_SERVER("enable_rpc_server", Settings.SECTION_DEBUG, false),
|
||||
CUSTOM_LAYOUT("custom_layout",Settings.SECTION_LAYOUT,false),
|
||||
OVERLAY_SHOW_FPS("overlay_show_fps", Settings.SECTION_LAYOUT, true),
|
||||
OVERLAY_SHOW_FRAMETIME("overlay_show_frame_time", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_SPEED("overlay_show_speed", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_APP_RAM_USAGE("overlay_show_app_ram_usage", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_AVAILABLE_RAM("overlay_show_available_ram", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_SHOW_BATTERY_TEMP("overlay_show_battery_temp", Settings.SECTION_LAYOUT, false),
|
||||
OVERLAY_BACKGROUND("overlay_background", Settings.SECTION_LAYOUT, false),
|
||||
SWAP_EYES_3D("swap_eyes_3d",Settings.SECTION_RENDERER,false),
|
||||
PERF_OVERLAY_ENABLE("performance_overlay_enable", Settings.SECTION_LAYOUT, false),
|
||||
PERF_OVERLAY_SHOW_FPS("performance_overlay_show_fps", Settings.SECTION_LAYOUT, true),
|
||||
PERF_OVERLAY_SHOW_FRAMETIME("performance_overlay_show_frame_time", Settings.SECTION_LAYOUT, false),
|
||||
PERF_OVERLAY_SHOW_SPEED("performance_overlay_show_speed", Settings.SECTION_LAYOUT, false),
|
||||
PERF_OVERLAY_SHOW_APP_RAM_USAGE("performance_overlay_show_app_ram_usage", Settings.SECTION_LAYOUT, false),
|
||||
PERF_OVERLAY_SHOW_AVAILABLE_RAM("performance_overlay_show_available_ram", Settings.SECTION_LAYOUT, false),
|
||||
PERF_OVERLAY_SHOW_BATTERY_TEMP("performance_overlay_show_battery_temp", Settings.SECTION_LAYOUT, false),
|
||||
PERF_OVERLAY_BACKGROUND("performance_overlay_background", Settings.SECTION_LAYOUT, false),
|
||||
DELAY_START_LLE_MODULES("delay_start_for_lle_modules", Settings.SECTION_DEBUG, true),
|
||||
DETERMINISTIC_ASYNC_OPERATIONS("deterministic_async_operations", Settings.SECTION_DEBUG, false),
|
||||
REQUIRED_ONLINE_LLE_MODULES("enable_required_online_lle_modules", Settings.SECTION_SYSTEM, false),
|
||||
|
|
@ -43,16 +45,17 @@ enum class BooleanSetting(
|
|||
CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, true),
|
||||
HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, true),
|
||||
SHADER_JIT("use_shader_jit", Settings.SECTION_RENDERER, true),
|
||||
VSYNC("use_vsync_new", Settings.SECTION_RENDERER, true),
|
||||
VSYNC("use_vsync", Settings.SECTION_RENDERER, false),
|
||||
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, true),
|
||||
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, false),
|
||||
DISABLE_RIGHT_EYE_RENDER("disable_right_eye_render", Settings.SECTION_RENDERER, false),
|
||||
USE_ARTIC_BASE_CONTROLLER("use_artic_base_controller", Settings.SECTION_CONTROLS, false),
|
||||
UPRIGHT_SCREEN("upright_screen", Settings.SECTION_LAYOUT, false),
|
||||
COMPRESS_INSTALLED_CIA_CONTENT("compress_cia_installs", Settings.SECTION_STORAGE, false),
|
||||
ANDROID_HIDE_IMAGES("android_hide_images", Settings.SECTION_CORE, false),
|
||||
APPLY_REGION_FREE_PATCH("apply_region_free_patch", Settings.SECTION_SYSTEM, true),
|
||||
ENABLE_COMBO_KEY("enable_combo_key", Settings.SECTION_CONTROLS, true);
|
||||
|
||||
|
||||
override var boolean: Boolean = defaultValue
|
||||
|
||||
override val valueAsString: String
|
||||
|
|
@ -85,6 +88,9 @@ enum class BooleanSetting(
|
|||
SHADERS_ACCURATE_MUL,
|
||||
USE_ARTIC_BASE_CONTROLLER,
|
||||
COMPRESS_INSTALLED_CIA_CONTENT,
|
||||
ANDROID_HIDE_IMAGES,
|
||||
PERF_OVERLAY_ENABLE, // Works in overlay options, but not from the settings menu
|
||||
APPLY_REGION_FREE_PATCH
|
||||
)
|
||||
|
||||
fun from(key: String): BooleanSetting? =
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -10,6 +10,10 @@ enum class FloatSetting(
|
|||
override val defaultValue: Float
|
||||
) : AbstractFloatSetting {
|
||||
LARGE_SCREEN_PROPORTION("large_screen_proportion",Settings.SECTION_LAYOUT,2.25f),
|
||||
SECOND_SCREEN_OPACITY("custom_second_layer_opacity", Settings.SECTION_RENDERER, 100f),
|
||||
BACKGROUND_RED("bg_red", Settings.SECTION_RENDERER, 0f),
|
||||
BACKGROUND_BLUE("bg_blue", Settings.SECTION_RENDERER, 0f),
|
||||
BACKGROUND_GREEN("bg_green", Settings.SECTION_RENDERER, 0f),
|
||||
EMPTY_SETTING("", "", 0.0f);
|
||||
|
||||
override var float: Float = defaultValue
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ enum class IntSetting(
|
|||
CAMERA_OUTER_RIGHT_FLIP("camera_outer_right_flip", Settings.SECTION_CAMERA, 0),
|
||||
GRAPHICS_API("graphics_api", Settings.SECTION_RENDERER, 1),
|
||||
RESOLUTION_FACTOR("resolution_factor", Settings.SECTION_RENDERER, 1),
|
||||
STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 0),
|
||||
STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 2),
|
||||
STEREOSCOPIC_3D_DEPTH("factor_3d", Settings.SECTION_RENDERER, 0),
|
||||
STEPS_PER_HOUR("steps_per_hour", Settings.SECTION_SYSTEM, 0),
|
||||
CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85),
|
||||
|
|
@ -35,6 +35,7 @@ enum class IntSetting(
|
|||
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),
|
||||
SECONDARY_DISPLAY_LAYOUT("secondary_display_layout",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),
|
||||
PORTRAIT_TOP_WIDTH("custom_portrait_top_width",Settings.SECTION_LAYOUT,800),
|
||||
|
|
@ -52,6 +53,7 @@ enum class IntSetting(
|
|||
ORIENTATION_OPTION("screen_orientation", Settings.SECTION_LAYOUT, 2),
|
||||
TURBO_LIMIT("turbo_limit", Settings.SECTION_CORE, 200),
|
||||
PERFORMANCE_OVERLAY_POSITION("performance_overlay_position", Settings.SECTION_LAYOUT, 0),
|
||||
RENDER_3D_WHICH_DISPLAY("render_3d_which_display",Settings.SECTION_RENDERER,0),
|
||||
ASPECT_RATIO("aspect_ratio", Settings.SECTION_LAYOUT, 0);
|
||||
|
||||
override var int: Int = defaultValue
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import org.citra.citra_emu.NativeLibrary
|
|||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.features.hotkeys.Hotkey
|
||||
import org.citra.citra_emu.features.settings.model.AbstractSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
|
||||
class InputBindingSetting(
|
||||
|
|
@ -162,12 +163,14 @@ class InputBindingSetting(
|
|||
fun removeOldMapping() {
|
||||
// Try remove all possible keys we wrote for this setting
|
||||
val oldKey = preferences.getString(reverseKey, "")
|
||||
(setting as AbstractStringSetting).string = ""
|
||||
if (oldKey != "") {
|
||||
preferences.edit()
|
||||
.remove(abstractSetting.key) // Used for ui text
|
||||
.remove(oldKey) // Used for button mapping
|
||||
.remove(oldKey + "_GuestOrientation") // Used for axis orientation
|
||||
.remove(oldKey + "_GuestButton") // Used for axis button
|
||||
.remove(oldKey + "_Inverted") // used for axis inversion
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
|
@ -201,7 +204,7 @@ class InputBindingSetting(
|
|||
/**
|
||||
* Helper function to write a gamepad axis mapping for the setting.
|
||||
*/
|
||||
private fun writeAxisMapping(axis: Int, value: Int) {
|
||||
private fun writeAxisMapping(axis: Int, value: Int, inverted: Boolean) {
|
||||
// Cleanup old mapping
|
||||
removeOldMapping()
|
||||
|
||||
|
|
@ -209,6 +212,7 @@ class InputBindingSetting(
|
|||
preferences.edit()
|
||||
.putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1)
|
||||
.putInt(getInputAxisButtonKey(axis), value)
|
||||
.putBoolean(getInputAxisInvertedKey(axis),inverted)
|
||||
// Write next reverse mapping for future cleanup
|
||||
.putString(reverseKey, getInputAxisKey(axis))
|
||||
.apply()
|
||||
|
|
@ -236,7 +240,7 @@ class InputBindingSetting(
|
|||
*
|
||||
* @param device InputDevice from which the input event originated.
|
||||
* @param motionRange MotionRange of the movement
|
||||
* @param axisDir Either '-' or '+' (currently unused)
|
||||
* @param axisDir Either '-' or '+'
|
||||
*/
|
||||
fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) {
|
||||
if (!isAxisMappingSupported()) {
|
||||
|
|
@ -252,8 +256,10 @@ class InputBindingSetting(
|
|||
} else {
|
||||
buttonCode
|
||||
}
|
||||
writeAxisMapping(motionRange.axis, button)
|
||||
val uiString = "${device.name}: Axis ${motionRange.axis}"
|
||||
// use UP (-) to map vertical, but use RIGHT (+) to map horizontal
|
||||
val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+'
|
||||
writeAxisMapping(motionRange.axis, button, inverted)
|
||||
val uiString = "${device.name}: Axis ${motionRange.axis}" + axisDir
|
||||
value = uiString
|
||||
}
|
||||
|
||||
|
|
@ -309,6 +315,11 @@ class InputBindingSetting(
|
|||
*/
|
||||
fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an whether a gamepad axis is inverted.
|
||||
*/
|
||||
fun getInputAxisInvertedKey(axis: Int): String = "${getInputAxisKey(axis)}_Inverted"
|
||||
|
||||
/**
|
||||
* Helper function to get the settings key for an gamepad axis orientation.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -10,7 +10,6 @@ import org.citra.citra_emu.features.settings.model.AbstractSetting
|
|||
import org.citra.citra_emu.features.settings.model.FloatSetting
|
||||
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SliderSetting(
|
||||
setting: AbstractSetting?,
|
||||
|
|
@ -27,7 +26,8 @@ class SliderSetting(
|
|||
val selectedFloat: Float
|
||||
get() {
|
||||
val setting = setting ?: return defaultValue!!.toFloat()
|
||||
return when (setting) {
|
||||
|
||||
val ret = when (setting) {
|
||||
is AbstractIntSetting -> setting.int.toFloat()
|
||||
is FloatSetting -> setting.float
|
||||
is ScaledFloatSetting -> setting.float
|
||||
|
|
@ -36,8 +36,8 @@ class SliderSetting(
|
|||
-1f
|
||||
}
|
||||
}
|
||||
return ret.coerceIn(min.toFloat(), max.toFloat())
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to the backing int. If that int was previously null,
|
||||
* initializes a new one and returns it, so it can be added to the Hashmap.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package org.citra.citra_emu.features.settings.ui
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
|
|
@ -37,6 +38,7 @@ import org.citra.citra_emu.features.settings.utils.SettingsFile
|
|||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.InsetsHelper
|
||||
import org.citra.citra_emu.utils.RefreshRateUtil
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
|
||||
class SettingsActivity : AppCompatActivity(), SettingsActivityView {
|
||||
|
|
@ -49,6 +51,8 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
|
|||
override val settings: Settings get() = settingsViewModel.settings
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
RefreshRateUtil.enforceRefreshRate(this)
|
||||
|
||||
ThemeUtil.setTheme(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
|||
|
|
@ -4,14 +4,19 @@
|
|||
|
||||
package org.citra.citra_emu.features.settings.ui
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
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.FileUtil
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
import org.citra.citra_emu.utils.TurboHelper
|
||||
|
||||
class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
|
||||
|
|
@ -60,6 +65,32 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView)
|
|||
loadSettingsUI()
|
||||
}
|
||||
|
||||
private fun updateAndroidImageVisibility() {
|
||||
val dataDirTreeUri: Uri
|
||||
val dataDirDocument: DocumentFile
|
||||
val nomediaFileDocument: DocumentFile?
|
||||
val nomediaFileExists: Boolean
|
||||
try {
|
||||
dataDirTreeUri = PermissionsHandler.citraDirectory
|
||||
dataDirDocument = DocumentFile.fromTreeUri(CitraApplication.appContext, dataDirTreeUri)!!
|
||||
nomediaFileDocument = dataDirDocument.findFile(".nomedia")
|
||||
nomediaFileExists = (nomediaFileDocument != null)
|
||||
} catch (e: Exception) {
|
||||
Log.error("[SettingsActivity]: Error occurred while trying to find .nomedia, error: " + e.message)
|
||||
return
|
||||
}
|
||||
|
||||
if (BooleanSetting.ANDROID_HIDE_IMAGES.boolean) {
|
||||
if (!nomediaFileExists) {
|
||||
Log.info("[SettingsActivity]: Attempting to create .nomedia in user data directory")
|
||||
FileUtil.createFile(dataDirTreeUri.toString(), ".nomedia")
|
||||
}
|
||||
} else if (nomediaFileExists) {
|
||||
Log.info("[SettingsActivity]: Attempting to delete .nomedia in user data directory")
|
||||
nomediaFileDocument!!.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop(finishing: Boolean) {
|
||||
if (finishing && shouldSave) {
|
||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
||||
|
|
@ -67,6 +98,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)
|
||||
updateAndroidImageVisibility()
|
||||
TurboHelper.reloadTurbo(false) // TODO: Can this go somewhere else? -OS
|
||||
}
|
||||
NativeLibrary.reloadSettings()
|
||||
|
|
|
|||
|
|
@ -566,6 +566,21 @@ class SettingsAdapter(
|
|||
return true
|
||||
}
|
||||
|
||||
fun onInputBindingLongClick(setting: InputBindingSetting, position: Int): Boolean {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.reset_setting_confirmation)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
setting.removeOldMapping()
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.loadSettingsList()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun onClickDisabledSetting(isRuntimeDisabled: Boolean) {
|
||||
val titleId = if (isRuntimeDisabled)
|
||||
R.string.setting_not_editable
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ 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
|
||||
import org.citra.citra_emu.display.PortraitScreenLayout
|
||||
import org.citra.citra_emu.display.ScreenLayout
|
||||
import org.citra.citra_emu.display.StereoMode
|
||||
import org.citra.citra_emu.display.StereoWhichDisplay
|
||||
import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractIntSetting
|
||||
import org.citra.citra_emu.features.settings.model.AbstractMultiStringSetting
|
||||
|
|
@ -49,7 +49,6 @@ import org.citra.citra_emu.utils.BirthdayMonth
|
|||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.SystemSaveGame
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
|
||||
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
|
||||
private var menuTag: String? = null
|
||||
|
|
@ -116,20 +115,24 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
}
|
||||
|
||||
/** Returns the portrait mode width */
|
||||
private fun getWidth(): Int {
|
||||
val dm = Resources.getSystem().displayMetrics;
|
||||
return if (dm.widthPixels < dm.heightPixels)
|
||||
dm.widthPixels
|
||||
else
|
||||
dm.heightPixels
|
||||
private fun getDimensions(): IntArray {
|
||||
val dm = Resources.getSystem().displayMetrics
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val wm = settingsActivity.windowManager.maximumWindowMetrics
|
||||
val height = wm.bounds.height().coerceAtLeast(dm.heightPixels)
|
||||
val width = wm.bounds.width().coerceAtLeast(dm.widthPixels)
|
||||
intArrayOf(width, height)
|
||||
} else {
|
||||
intArrayOf(dm.widthPixels, dm.heightPixels)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHeight(): Int {
|
||||
val dm = Resources.getSystem().displayMetrics;
|
||||
return if (dm.widthPixels < dm.heightPixels)
|
||||
dm.heightPixels
|
||||
else
|
||||
dm.widthPixels
|
||||
private fun getSmallerDimension(): Int {
|
||||
return getDimensions().min()
|
||||
}
|
||||
|
||||
private fun getLargerDimension(): Int {
|
||||
return getDimensions().max()
|
||||
}
|
||||
|
||||
private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
|
||||
|
|
@ -253,6 +256,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
IntSetting.TURBO_LIMIT.defaultValue.toFloat()
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.ANDROID_HIDE_IMAGES,
|
||||
R.string.android_hide_images,
|
||||
R.string.android_hide_images_description,
|
||||
BooleanSetting.ANDROID_HIDE_IMAGES.key,
|
||||
BooleanSetting.ANDROID_HIDE_IMAGES.defaultValue
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +362,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.array.regionValues,
|
||||
)
|
||||
)
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.APPLY_REGION_FREE_PATCH,
|
||||
R.string.apply_region_free_patch,
|
||||
R.string.apply_region_free_patch_desc,
|
||||
BooleanSetting.APPLY_REGION_FREE_PATCH.key,
|
||||
BooleanSetting.APPLY_REGION_FREE_PATCH.defaultValue
|
||||
)
|
||||
)
|
||||
val systemCountrySetting = object : AbstractShortSetting {
|
||||
override var short: Short
|
||||
get() {
|
||||
|
|
@ -963,17 +984,30 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
|
||||
add(HeaderSetting(R.string.stereoscopy))
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.RENDER_3D_WHICH_DISPLAY,
|
||||
R.string.render_3d_which_display,
|
||||
R.string.render_3d_which_display_description,
|
||||
R.array.render3dWhichDisplay,
|
||||
R.array.render3dDisplayValues,
|
||||
IntSetting.RENDER_3D_WHICH_DISPLAY.key,
|
||||
IntSetting.RENDER_3D_WHICH_DISPLAY.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.STEREOSCOPIC_3D_MODE,
|
||||
R.string.render3d,
|
||||
0,
|
||||
R.string.render3d_description,
|
||||
R.array.render3dModes,
|
||||
R.array.render3dValues,
|
||||
IntSetting.STEREOSCOPIC_3D_MODE.key,
|
||||
IntSetting.STEREOSCOPIC_3D_MODE.defaultValue
|
||||
IntSetting.STEREOSCOPIC_3D_MODE.defaultValue,
|
||||
isEnabled = IntSetting.RENDER_3D_WHICH_DISPLAY.int != StereoWhichDisplay.NONE.int
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SliderSetting(
|
||||
IntSetting.STEREOSCOPIC_3D_DEPTH,
|
||||
|
|
@ -996,6 +1030,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.SWAP_EYES_3D,
|
||||
R.string.swap_eyes_3d,
|
||||
R.string.swap_eyes_3d_description,
|
||||
BooleanSetting.SWAP_EYES_3D.key,
|
||||
BooleanSetting.SWAP_EYES_3D.defaultValue,
|
||||
isEnabled = IntSetting.RENDER_3D_WHICH_DISPLAY.int != StereoWhichDisplay.NONE.int
|
||||
)
|
||||
)
|
||||
|
||||
add(HeaderSetting(R.string.cardboard_vr))
|
||||
add(
|
||||
SliderSetting(
|
||||
|
|
@ -1006,7 +1051,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
100,
|
||||
"%",
|
||||
IntSetting.CARDBOARD_SCREEN_SIZE.key,
|
||||
IntSetting.CARDBOARD_SCREEN_SIZE.defaultValue.toFloat()
|
||||
IntSetting.CARDBOARD_SCREEN_SIZE.defaultValue.toFloat(),
|
||||
isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
|
@ -1018,7 +1064,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
100,
|
||||
"%",
|
||||
IntSetting.CARDBOARD_X_SHIFT.key,
|
||||
IntSetting.CARDBOARD_X_SHIFT.defaultValue.toFloat()
|
||||
IntSetting.CARDBOARD_X_SHIFT.defaultValue.toFloat(),
|
||||
isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
|
@ -1030,7 +1077,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
100,
|
||||
"%",
|
||||
IntSetting.CARDBOARD_Y_SHIFT.key,
|
||||
IntSetting.CARDBOARD_Y_SHIFT.defaultValue.toFloat()
|
||||
IntSetting.CARDBOARD_Y_SHIFT.defaultValue.toFloat(),
|
||||
isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -1144,6 +1192,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.SECONDARY_DISPLAY_LAYOUT,
|
||||
R.string.emulation_switch_secondary_layout,
|
||||
R.string.emulation_switch_secondary_layout_description,
|
||||
R.array.secondaryLayouts,
|
||||
R.array.secondaryLayoutValues,
|
||||
IntSetting.SECONDARY_DISPLAY_LAYOUT.key,
|
||||
IntSetting.SECONDARY_DISPLAY_LAYOUT.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.ASPECT_RATIO,
|
||||
|
|
@ -1153,7 +1212,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.array.aspectRatioValues,
|
||||
IntSetting.ASPECT_RATIO.key,
|
||||
IntSetting.ASPECT_RATIO.defaultValue,
|
||||
isEnabled = IntSetting.SCREEN_LAYOUT.int == 1,
|
||||
isEnabled = IntSetting.SCREEN_LAYOUT.int == ScreenLayout.SINGLE_SCREEN.int,
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
|
@ -1191,6 +1250,89 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
FloatSetting.LARGE_SCREEN_PROPORTION.defaultValue
|
||||
)
|
||||
)
|
||||
add(
|
||||
SliderSetting(
|
||||
FloatSetting.SECOND_SCREEN_OPACITY,
|
||||
R.string.second_screen_opacity,
|
||||
R.string.second_screen_opacity_description,
|
||||
0,
|
||||
100,
|
||||
"%",
|
||||
FloatSetting.SECOND_SCREEN_OPACITY.key,
|
||||
FloatSetting.SECOND_SCREEN_OPACITY.defaultValue,
|
||||
isEnabled = IntSetting.SCREEN_LAYOUT.int == ScreenLayout.CUSTOM_LAYOUT.int
|
||||
)
|
||||
)
|
||||
add(HeaderSetting(R.string.bg_color, R.string.bg_color_description))
|
||||
val bgRedSetting = object : AbstractIntSetting {
|
||||
override var int: Int
|
||||
get() = (FloatSetting.BACKGROUND_RED.float * 255).toInt()
|
||||
set(value) {
|
||||
FloatSetting.BACKGROUND_RED.float = value.toFloat() / 255
|
||||
settings.saveSetting(FloatSetting.BACKGROUND_RED, SettingsFile.FILE_NAME_CONFIG)
|
||||
}
|
||||
override val key = null
|
||||
override val section = null
|
||||
override val isRuntimeEditable = false
|
||||
override val valueAsString = int.toString()
|
||||
override val defaultValue = FloatSetting.BACKGROUND_RED.defaultValue
|
||||
}
|
||||
add(
|
||||
SliderSetting(
|
||||
bgRedSetting,
|
||||
R.string.bg_red,
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
""
|
||||
)
|
||||
)
|
||||
val bgGreenSetting = object : AbstractIntSetting {
|
||||
override var int: Int
|
||||
get() = (FloatSetting.BACKGROUND_GREEN.float * 255).toInt()
|
||||
set(value) {
|
||||
FloatSetting.BACKGROUND_GREEN.float = value.toFloat() / 255
|
||||
settings.saveSetting(FloatSetting.BACKGROUND_GREEN, SettingsFile.FILE_NAME_CONFIG)
|
||||
}
|
||||
override val key = null
|
||||
override val section = null
|
||||
override val isRuntimeEditable = false
|
||||
override val valueAsString = int.toString()
|
||||
override val defaultValue = FloatSetting.BACKGROUND_GREEN.defaultValue
|
||||
}
|
||||
add(
|
||||
SliderSetting(
|
||||
bgGreenSetting,
|
||||
R.string.bg_green,
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
""
|
||||
)
|
||||
)
|
||||
val bgBlueSetting = object : AbstractIntSetting {
|
||||
override var int: Int
|
||||
get() = (FloatSetting.BACKGROUND_BLUE.float * 255).toInt()
|
||||
set(value) {
|
||||
FloatSetting.BACKGROUND_BLUE.float = value.toFloat() / 255
|
||||
settings.saveSetting(FloatSetting.BACKGROUND_BLUE, SettingsFile.FILE_NAME_CONFIG)
|
||||
}
|
||||
override val key = null
|
||||
override val section = null
|
||||
override val isRuntimeEditable = false
|
||||
override val valueAsString = int.toString()
|
||||
override val defaultValue = FloatSetting.BACKGROUND_BLUE.defaultValue
|
||||
}
|
||||
add(
|
||||
SliderSetting(
|
||||
bgBlueSetting,
|
||||
R.string.bg_blue,
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
""
|
||||
)
|
||||
)
|
||||
add(
|
||||
SubmenuSetting(
|
||||
R.string.performance_overlay_options,
|
||||
|
|
@ -1226,38 +1368,29 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
|
||||
add(
|
||||
SwitchSetting(
|
||||
object : AbstractBooleanSetting {
|
||||
override val key = "EmulationMenuSettings_showPerfPerformanceOverlay"
|
||||
override val section = Settings.SECTION_LAYOUT
|
||||
override val defaultValue = false
|
||||
override var boolean: Boolean
|
||||
get() = EmulationMenuSettings.showPerformanceOverlay
|
||||
set(value) { EmulationMenuSettings.showPerformanceOverlay = value }
|
||||
override val isRuntimeEditable = true
|
||||
override val valueAsString: String get() = boolean.toString()
|
||||
},
|
||||
BooleanSetting.PERF_OVERLAY_ENABLE,
|
||||
R.string.performance_overlay_enable,
|
||||
0,
|
||||
"EmulationMenuSettings_showPerfPerformanceOverlay",
|
||||
false
|
||||
BooleanSetting.PERF_OVERLAY_ENABLE.key,
|
||||
BooleanSetting.PERF_OVERLAY_ENABLE.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_BACKGROUND,
|
||||
R.string.overlay_background,
|
||||
R.string.overlay_background_description,
|
||||
"overlay_background",
|
||||
false
|
||||
BooleanSetting.PERF_OVERLAY_BACKGROUND,
|
||||
R.string.performance_overlay_background,
|
||||
R.string.performance_overlay_background_description,
|
||||
BooleanSetting.PERF_OVERLAY_BACKGROUND.key,
|
||||
BooleanSetting.PERF_OVERLAY_BACKGROUND.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.PERFORMANCE_OVERLAY_POSITION,
|
||||
R.string.overlay_position,
|
||||
R.string.overlay_position_description,
|
||||
R.string.performance_overlay_position,
|
||||
R.string.performance_overlay_position_description,
|
||||
R.array.statsPosition,
|
||||
R.array.statsPositionValues,
|
||||
)
|
||||
|
|
@ -1268,61 +1401,61 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_FPS,
|
||||
R.string.overlay_show_fps,
|
||||
R.string.overlay_show_fps_description,
|
||||
"overlay_show_fps",
|
||||
true
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_FPS,
|
||||
R.string.performance_overlay_show_fps,
|
||||
R.string.performance_overlay_show_fps_description,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_FPS.key,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_FPS.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_FRAMETIME,
|
||||
R.string.overlay_show_frametime,
|
||||
R.string.overlay_show_frametime_description,
|
||||
"overlay_show_frame_time",
|
||||
true
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME,
|
||||
R.string.performance_overlay_show_frametime,
|
||||
R.string.performance_overlay_show_frametime_description,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME.key,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_SPEED,
|
||||
R.string.overlay_show_speed,
|
||||
R.string.overlay_show_speed_description,
|
||||
"overlay_show_speed",
|
||||
false
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_SPEED,
|
||||
R.string.performance_overlay_show_speed,
|
||||
R.string.performance_overlay_show_speed_description,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_SPEED.key,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_SPEED.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_APP_RAM_USAGE,
|
||||
R.string.overlay_show_app_ram_usage,
|
||||
R.string.overlay_show_app_ram_usage_description,
|
||||
"overlay_show_app_ram_usage",
|
||||
false
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE,
|
||||
R.string.performance_overlay_show_app_ram_usage,
|
||||
R.string.performance_overlay_show_app_ram_usage_description,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE.key,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_AVAILABLE_RAM,
|
||||
R.string.overlay_show_available_ram,
|
||||
R.string.overlay_show_available_ram_description,
|
||||
"overlay_show_available_ram",
|
||||
false
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM,
|
||||
R.string.performance_overlay_show_available_ram,
|
||||
R.string.performance_overlay_show_available_ram_description,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM.key,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM.defaultValue
|
||||
)
|
||||
)
|
||||
|
||||
add(
|
||||
SwitchSetting(
|
||||
BooleanSetting.OVERLAY_SHOW_BATTERY_TEMP,
|
||||
R.string.overlay_show_battery_temp,
|
||||
R.string.overlay_show_battery_temp_description,
|
||||
"overlay_show_battery_temp",
|
||||
false
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP,
|
||||
R.string.performance_overlay_show_battery_temp,
|
||||
R.string.performance_overlay_show_battery_temp_description,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP.key,
|
||||
BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP.defaultValue
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -1338,7 +1471,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_x,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_TOP_X.key,
|
||||
IntSetting.LANDSCAPE_TOP_X.defaultValue.toFloat()
|
||||
|
|
@ -1350,7 +1483,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_y,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_TOP_Y.key,
|
||||
IntSetting.LANDSCAPE_TOP_Y.defaultValue.toFloat()
|
||||
|
|
@ -1362,7 +1495,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_width,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_TOP_WIDTH.key,
|
||||
IntSetting.LANDSCAPE_TOP_WIDTH.defaultValue.toFloat()
|
||||
|
|
@ -1374,7 +1507,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_height,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_TOP_HEIGHT.key,
|
||||
IntSetting.LANDSCAPE_TOP_HEIGHT.defaultValue.toFloat()
|
||||
|
|
@ -1387,7 +1520,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_x,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_BOTTOM_X.key,
|
||||
IntSetting.LANDSCAPE_BOTTOM_X.defaultValue.toFloat()
|
||||
|
|
@ -1399,7 +1532,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_y,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_BOTTOM_Y.key,
|
||||
IntSetting.LANDSCAPE_BOTTOM_Y.defaultValue.toFloat()
|
||||
|
|
@ -1411,7 +1544,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_width,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_BOTTOM_WIDTH.key,
|
||||
IntSetting.LANDSCAPE_BOTTOM_WIDTH.defaultValue.toFloat()
|
||||
|
|
@ -1423,7 +1556,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_height,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.LANDSCAPE_BOTTOM_HEIGHT.key,
|
||||
IntSetting.LANDSCAPE_BOTTOM_HEIGHT.defaultValue.toFloat()
|
||||
|
|
@ -1443,7 +1576,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_x,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_TOP_X.key,
|
||||
IntSetting.PORTRAIT_TOP_X.defaultValue.toFloat()
|
||||
|
|
@ -1455,7 +1588,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_y,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_TOP_Y.key,
|
||||
IntSetting.PORTRAIT_TOP_Y.defaultValue.toFloat()
|
||||
|
|
@ -1467,7 +1600,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_width,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_TOP_WIDTH.key,
|
||||
IntSetting.PORTRAIT_TOP_WIDTH.defaultValue.toFloat()
|
||||
|
|
@ -1479,7 +1612,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_height,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_TOP_HEIGHT.key,
|
||||
IntSetting.PORTRAIT_TOP_HEIGHT.defaultValue.toFloat()
|
||||
|
|
@ -1492,7 +1625,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_x,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_BOTTOM_X.key,
|
||||
IntSetting.PORTRAIT_BOTTOM_X.defaultValue.toFloat()
|
||||
|
|
@ -1504,7 +1637,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_y,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_BOTTOM_Y.key,
|
||||
IntSetting.PORTRAIT_BOTTOM_Y.defaultValue.toFloat()
|
||||
|
|
@ -1516,7 +1649,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_width,
|
||||
0,
|
||||
0,
|
||||
getWidth(),
|
||||
getSmallerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_BOTTOM_WIDTH.key,
|
||||
IntSetting.PORTRAIT_BOTTOM_WIDTH.defaultValue.toFloat()
|
||||
|
|
@ -1528,7 +1661,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
|||
R.string.emulation_custom_layout_height,
|
||||
0,
|
||||
0,
|
||||
getHeight(),
|
||||
getLargerDimension(),
|
||||
"px",
|
||||
IntSetting.PORTRAIT_BOTTOM_HEIGHT.key,
|
||||
IntSetting.PORTRAIT_BOTTOM_HEIGHT.defaultValue.toFloat()
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter
|
|||
|
||||
override fun onLongClick(clicked: View): Boolean {
|
||||
if (setting.isEditable) {
|
||||
adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
|
||||
adapter.onInputBindingLongClick(setting, bindingAdapterPosition)
|
||||
} else {
|
||||
adapter.onClickDisabledSetting(!setting.isEditable)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 CitraDirectoryDialogFragment : DialogFragment() {
|
|||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
|
||||
if (!PermissionsHandler.hasWriteAccess(requireContext())) {
|
||||
(requireActivity() as MainActivity)?.openCitraDirectory?.launch(null)
|
||||
PermissionsHandler.compatibleSelectDirectory((requireActivity() as MainActivity).openCitraDirectory)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
// 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.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
|
||||
class CompressProgressDialogFragment : DialogFragment() {
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private var outputPath: String? = null
|
||||
private var isCompressing: Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
isCompressing = it.getBoolean(ARG_IS_COMPRESSING, true)
|
||||
outputPath = it.getString(ARG_OUTPUT_PATH)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_compress_progress, null)
|
||||
progressBar = view.findViewById(R.id.compress_progress)
|
||||
val label = view.findViewById<android.widget.TextView>(R.id.compress_label)
|
||||
label.text = if (isCompressing) getString(R.string.compressing) else getString(R.string.decompressing)
|
||||
|
||||
isCancelable = false
|
||||
progressBar.isIndeterminate = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
combine(CompressProgressDialogViewModel.total, CompressProgressDialogViewModel.progress) { total, progress ->
|
||||
total to progress
|
||||
}.collectLatest { (total, progress) ->
|
||||
if (total <= 0) {
|
||||
progressBar.isIndeterminate = true
|
||||
label.visibility = View.GONE
|
||||
} else {
|
||||
progressBar.isIndeterminate = false
|
||||
label.visibility = View.VISIBLE
|
||||
progressBar.max = total
|
||||
progressBar.setProgress(progress, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(android.R.string.cancel) { _: android.content.DialogInterface, _: Int ->
|
||||
outputPath?.let { path ->
|
||||
NativeLibrary.deleteDocument(path)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "CompressProgressDialog"
|
||||
private const val ARG_IS_COMPRESSING = "isCompressing"
|
||||
private const val ARG_OUTPUT_PATH = "outputPath"
|
||||
|
||||
fun newInstance(isCompressing: Boolean, outputPath: String?): CompressProgressDialogFragment {
|
||||
val frag = CompressProgressDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean(ARG_IS_COMPRESSING, isCompressing)
|
||||
args.putString(ARG_OUTPUT_PATH, outputPath)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import android.content.IntentFilter
|
|||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
|
@ -66,6 +67,7 @@ import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
|||
import org.citra.citra_emu.display.ScreenLayout
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
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
|
||||
|
|
@ -100,6 +102,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
|
||||
private val emulationViewModel: EmulationViewModel by activityViewModels()
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
private val settings get() = settingsViewModel.settings
|
||||
|
||||
private val onPause = Runnable{ togglePause() }
|
||||
private val onShutdown = Runnable{ emulationState.stop() }
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
|
@ -139,6 +145,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
}
|
||||
}
|
||||
|
||||
val insertedCartridge = preferences.getString("insertedCartridge", "")
|
||||
NativeLibrary.setInsertedCartridge(insertedCartridge ?: "")
|
||||
|
||||
try {
|
||||
game = args.game ?: intentGame!!
|
||||
} catch (e: NullPointerException) {
|
||||
|
|
@ -155,9 +164,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
retainInstance = true
|
||||
emulationState = EmulationState(game.path)
|
||||
emulationActivity = requireActivity() as EmulationActivity
|
||||
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settingsViewModel.settings)
|
||||
EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() })
|
||||
EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() })
|
||||
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings)
|
||||
EmulationLifecycleUtil.addPauseResumeHook(onPause)
|
||||
EmulationLifecycleUtil.addShutdownHook(onShutdown)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
|
@ -507,6 +516,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
EmulationLifecycleUtil.removeHook(onPause)
|
||||
EmulationLifecycleUtil.removeHook(onShutdown)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun setupCitraDirectoriesThenStartEmulation() {
|
||||
val directoryInitializationState = DirectoryInitialization.start()
|
||||
if (directoryInitializationState ===
|
||||
|
|
@ -662,7 +677,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
popupMenu.menu.apply {
|
||||
findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
|
||||
findItem(R.id.menu_performance_overlay_show).isChecked =
|
||||
EmulationMenuSettings.showPerformanceOverlay
|
||||
BooleanSetting.PERF_OVERLAY_ENABLE.boolean
|
||||
findItem(R.id.menu_haptic_feedback).isChecked = EmulationMenuSettings.hapticFeedback
|
||||
findItem(R.id.menu_emulation_joystick_rel_center).isChecked =
|
||||
EmulationMenuSettings.joystickRelCenter
|
||||
|
|
@ -679,7 +694,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
}
|
||||
|
||||
R.id.menu_performance_overlay_show -> {
|
||||
EmulationMenuSettings.showPerformanceOverlay = !EmulationMenuSettings.showPerformanceOverlay
|
||||
BooleanSetting.PERF_OVERLAY_ENABLE.boolean = !BooleanSetting.PERF_OVERLAY_ENABLE.boolean
|
||||
settings.saveSetting(BooleanSetting.PERF_OVERLAY_ENABLE, SettingsFile.FILE_NAME_CONFIG)
|
||||
updateShowPerformanceOverlay()
|
||||
true
|
||||
}
|
||||
|
|
@ -1017,7 +1033,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
enabledButtons[i] = preferences.getBoolean("buttonToggle$i", defaultValue)
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.emulation_toggle_controls)
|
||||
.setMultiChoiceItems(
|
||||
R.array.n3dsButtons, enabledButtons
|
||||
|
|
@ -1029,6 +1045,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
binding.surfaceInputOverlay.refreshControls()
|
||||
}
|
||||
.show()
|
||||
|
||||
// Band-aid fix for strange dialog flickering issue
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val displayMetrics = requireActivity().windowManager.currentWindowMetrics
|
||||
val displayHeight = displayMetrics.bounds.height()
|
||||
// The layout visually breaks if we try to set the height directly rather than like this.
|
||||
// Why? Fuck you, that's why!
|
||||
val newAttributes = dialog.window?.attributes
|
||||
newAttributes?.height = (displayHeight * 0.85f).toInt()
|
||||
dialog.window?.attributes = newAttributes
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAdjustScaleDialog(target: String) {
|
||||
|
|
@ -1207,7 +1234,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!)
|
||||
}
|
||||
|
||||
if (EmulationMenuSettings.showPerformanceOverlay) {
|
||||
if (BooleanSetting.PERF_OVERLAY_ENABLE.boolean) {
|
||||
val SYSTEM_FPS = 0
|
||||
val FPS = 1
|
||||
val SPEED = 2
|
||||
|
|
@ -1222,11 +1249,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
val perfStats = NativeLibrary.getPerfStats()
|
||||
val dividerString = "\u00A0\u2502 "
|
||||
if (perfStats[FPS] > 0) {
|
||||
if (BooleanSetting.OVERLAY_SHOW_FPS.boolean) {
|
||||
if (BooleanSetting.PERF_OVERLAY_SHOW_FPS.boolean) {
|
||||
sb.append(String.format("FPS:\u00A0%d", (perfStats[FPS] + 0.5).toInt()))
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_FRAMETIME.boolean) {
|
||||
if (BooleanSetting.PERF_OVERLAY_SHOW_FRAMETIME.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
sb.append(
|
||||
String.format(
|
||||
|
|
@ -1241,7 +1268,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
)
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_SPEED.boolean) {
|
||||
if (BooleanSetting.PERF_OVERLAY_SHOW_SPEED.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
sb.append(
|
||||
String.format(
|
||||
|
|
@ -1251,14 +1278,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
)
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_APP_RAM_USAGE.boolean) {
|
||||
if (BooleanSetting.PERF_OVERLAY_SHOW_APP_RAM_USAGE.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
val appRamUsage =
|
||||
File("/proc/self/statm").readLines()[0].split(' ')[1].toLong() * 4096 / 1000000
|
||||
sb.append("Process\u00A0RAM:\u00A0$appRamUsage\u00A0MB")
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_AVAILABLE_RAM.boolean) {
|
||||
if (BooleanSetting.PERF_OVERLAY_SHOW_AVAILABLE_RAM.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
context?.let { ctx ->
|
||||
val activityManager =
|
||||
|
|
@ -1271,14 +1298,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
|||
}
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_SHOW_BATTERY_TEMP.boolean) {
|
||||
if (BooleanSetting.PERF_OVERLAY_SHOW_BATTERY_TEMP.boolean) {
|
||||
if (sb.isNotEmpty()) sb.append(dividerString)
|
||||
val batteryTemp = getBatteryTemperature()
|
||||
val tempF = celsiusToFahrenheit(batteryTemp)
|
||||
sb.append(String.format("%.1f°C/%.1f°F", batteryTemp, tempF))
|
||||
}
|
||||
|
||||
if (BooleanSetting.OVERLAY_BACKGROUND.boolean) {
|
||||
if (BooleanSetting.PERF_OVERLAY_BACKGROUND.boolean) {
|
||||
binding.performanceOverlayShowText.setBackgroundResource(R.color.citra_transparent_black)
|
||||
} else {
|
||||
binding.performanceOverlayShowText.setBackgroundResource(0)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,12 @@ package org.citra.citra_emu.fragments
|
|||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
|
|
@ -30,14 +26,17 @@ 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.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.adapters.GameAdapter
|
||||
import org.citra.citra_emu.databinding.FragmentGamesBinding
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
|
||||
|
|
@ -47,7 +46,6 @@ class GamesFragment : Fragment() {
|
|||
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private var show3DSFileWarning: Boolean = true
|
||||
private lateinit var gameAdapter: GameAdapter
|
||||
|
||||
private val openImageLauncher = registerForActivityResult(
|
||||
|
|
@ -56,6 +54,58 @@ class GamesFragment : Fragment() {
|
|||
gameAdapter.handleShortcutImageResult(uri)
|
||||
}
|
||||
|
||||
private var shouldCompress: Boolean = true
|
||||
private var pendingCompressInvocation: String? = null
|
||||
|
||||
companion object {
|
||||
fun doCompression(fragment: Fragment, gamesViewModel: GamesViewModel, inputPath: String?, outputUri: Uri?, shouldCompress: Boolean) {
|
||||
if (outputUri != null) {
|
||||
CompressProgressDialogViewModel.reset()
|
||||
val dialog = CompressProgressDialogFragment.newInstance(shouldCompress, outputUri.toString())
|
||||
dialog.showNow(
|
||||
fragment.requireActivity().supportFragmentManager,
|
||||
CompressProgressDialogFragment.TAG
|
||||
)
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val status = if (shouldCompress) {
|
||||
NativeLibrary.compressFile(inputPath, outputUri.toString())
|
||||
} else {
|
||||
NativeLibrary.decompressFile(inputPath, outputUri.toString())
|
||||
}
|
||||
|
||||
fragment.requireActivity().runOnUiThread {
|
||||
dialog.dismiss()
|
||||
val resId = when (status) {
|
||||
NativeLibrary.CompressStatus.SUCCESS -> if (shouldCompress) R.string.compress_success else R.string.decompress_success
|
||||
NativeLibrary.CompressStatus.COMPRESS_UNSUPPORTED -> R.string.compress_unsupported
|
||||
NativeLibrary.CompressStatus.COMPRESS_ALREADY_COMPRESSED -> R.string.compress_already
|
||||
NativeLibrary.CompressStatus.COMPRESS_FAILED -> R.string.compress_failed
|
||||
NativeLibrary.CompressStatus.DECOMPRESS_UNSUPPORTED -> R.string.decompress_unsupported
|
||||
NativeLibrary.CompressStatus.DECOMPRESS_NOT_COMPRESSED -> R.string.decompress_not_compressed
|
||||
NativeLibrary.CompressStatus.DECOMPRESS_FAILED -> R.string.decompress_failed
|
||||
NativeLibrary.CompressStatus.INSTALLED_APPLICATION -> R.string.compress_decompress_installed_app
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setMessage(fragment.getString(resId))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
|
||||
gamesViewModel.reloadGames(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val onCompressDecompressLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri: Uri? ->
|
||||
doCompression(this, gamesViewModel, pendingCompressInvocation, uri, shouldCompress)
|
||||
pendingCompressInvocation = null
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialFadeThrough()
|
||||
|
|
@ -81,7 +131,12 @@ class GamesFragment : Fragment() {
|
|||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
openImageLauncher,
|
||||
onRequestCompressOrDecompress = { inputPath, suggestedName, shouldCompress ->
|
||||
pendingCompressInvocation = inputPath
|
||||
onCompressDecompressLauncher.launch(suggestedName)
|
||||
this.shouldCompress = shouldCompress
|
||||
}
|
||||
)
|
||||
|
||||
binding.gridGames.apply {
|
||||
|
|
@ -164,34 +219,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
// 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.Manifest
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.ui.main.MainActivity
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
|
||||
class GrantMissingFilesystemPermissionFragment : DialogFragment() {
|
||||
private lateinit var mainActivity: MainActivity
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
isCancelable = false
|
||||
|
||||
val requestPermissionFunction =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
{
|
||||
manageExternalStoragePermissionLauncher.launch(
|
||||
Intent(
|
||||
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
||||
Uri.fromParts("package", mainActivity.packageName, null)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
{ permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) }
|
||||
}
|
||||
|
||||
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.filesystem_permission_warning)
|
||||
.setMessage(R.string.filesystem_permission_lost)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
requestPermissionFunction()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private val manageExternalStoragePermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
}
|
||||
|
||||
private val permissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "GrantMissingFilesystemPermissionFragment"
|
||||
|
||||
fun newInstance(): GrantMissingFilesystemPermissionFragment {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
return GrantMissingFilesystemPermissionFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,7 +159,7 @@ class HomeSettingsFragment : Fragment() {
|
|||
R.string.select_citra_user_folder,
|
||||
R.string.select_citra_user_folder_home_description,
|
||||
R.drawable.ic_home,
|
||||
{ mainActivity?.openCitraDirectory?.launch(null) },
|
||||
{ PermissionsHandler.compatibleSelectDirectory(mainActivity.openCitraDirectory) },
|
||||
details = homeViewModel.userDir
|
||||
),
|
||||
HomeSetting(
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ package org.citra.citra_emu.fragments
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
|
@ -26,18 +28,19 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import info.debatty.java.stringsimilarity.Jaccard
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.adapters.GameAdapter
|
||||
import org.citra.citra_emu.databinding.FragmentSearchBinding
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import java.time.temporal.ChronoField
|
||||
import java.util.Locale
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
private var _binding: FragmentSearchBinding? = null
|
||||
|
|
@ -53,6 +56,15 @@ class SearchFragment : Fragment() {
|
|||
gameAdapter.handleShortcutImageResult(uri)
|
||||
}
|
||||
|
||||
private var shouldCompress: Boolean = true
|
||||
private var pendingCompressInvocation: String? = null
|
||||
private val onCompressDecompressLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri: Uri? ->
|
||||
GamesFragment.doCompression(this, gamesViewModel, pendingCompressInvocation, uri, shouldCompress)
|
||||
pendingCompressInvocation = null
|
||||
}
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
companion object {
|
||||
|
|
@ -85,7 +97,13 @@ class SearchFragment : Fragment() {
|
|||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
openImageLauncher,
|
||||
onRequestCompressOrDecompress = { inputPath, suggestedName, shouldCompress ->
|
||||
pendingCompressInvocation = inputPath
|
||||
onCompressDecompressLauncher.launch(suggestedName)
|
||||
this.shouldCompress = shouldCompress
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
binding.gridGamesSearch.apply {
|
||||
|
|
|
|||
|
|
@ -13,21 +13,25 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.ui.main.MainActivity
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
|
||||
class SelectUserDirectoryDialogFragment : DialogFragment() {
|
||||
class SelectUserDirectoryDialogFragment(titleOverride: Int? = null, descriptionOverride: Int? = null) : DialogFragment() {
|
||||
private lateinit var mainActivity: MainActivity
|
||||
|
||||
private val title = titleOverride ?: R.string.select_citra_user_folder
|
||||
private val description = descriptionOverride ?: R.string.selecting_user_directory_without_write_permissions
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
isCancelable = false
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.select_citra_user_folder)
|
||||
.setMessage(R.string.selecting_user_directory_without_write_permissions)
|
||||
.setTitle(title)
|
||||
.setMessage(description)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
mainActivity?.openCitraDirectoryLostPermission?.launch(null)
|
||||
PermissionsHandler.compatibleSelectDirectory(mainActivity.openCitraDirectoryLostPermission)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
|
@ -35,9 +39,10 @@ class SelectUserDirectoryDialogFragment : DialogFragment() {
|
|||
companion object {
|
||||
const val TAG = "SelectUserDirectoryDialogFragment"
|
||||
|
||||
fun newInstance(activity: FragmentActivity): SelectUserDirectoryDialogFragment {
|
||||
fun newInstance(activity: FragmentActivity, titleOverride: Int? = null, descriptionOverride: Int? = null):
|
||||
SelectUserDirectoryDialogFragment {
|
||||
ViewModelProvider(activity)[HomeViewModel::class.java].setPickingUserDir(true)
|
||||
return SelectUserDirectoryDialogFragment()
|
||||
return SelectUserDirectoryDialogFragment(titleOverride, descriptionOverride)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import android.content.pm.PackageManager
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
|
@ -30,7 +32,9 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import org.citra.citra_emu.BuildConfig
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.adapters.SetupAdapter
|
||||
import org.citra.citra_emu.databinding.FragmentSetupBinding
|
||||
|
|
@ -41,6 +45,7 @@ import org.citra.citra_emu.model.PageState
|
|||
import org.citra.citra_emu.model.SetupCallback
|
||||
import org.citra.citra_emu.model.SetupPage
|
||||
import org.citra.citra_emu.ui.main.MainActivity
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
import org.citra.citra_emu.utils.CitraDirectoryHelper
|
||||
import org.citra.citra_emu.utils.GameHelper
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
|
|
@ -142,7 +147,56 @@ class SetupFragment : Fragment() {
|
|||
false,
|
||||
0,
|
||||
pageButtons = mutableListOf<PageButton>().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (!BuildUtil.isGooglePlayBuild) {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_folder,
|
||||
R.string.filesystem_permission,
|
||||
R.string.filesystem_permission_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
manageExternalStoragePermissionLauncher.launch(
|
||||
Intent(
|
||||
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
||||
Uri.fromParts(
|
||||
"package",
|
||||
requireActivity().packageName,
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
},
|
||||
buttonState = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
}
|
||||
},
|
||||
isUnskippable = true,
|
||||
hasWarning = true,
|
||||
R.string.filesystem_permission_warning,
|
||||
R.string.filesystem_permission_warning_description,
|
||||
)
|
||||
)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_notification,
|
||||
|
|
@ -214,18 +268,35 @@ class SetupFragment : Fragment() {
|
|||
)
|
||||
},
|
||||
) {
|
||||
if (
|
||||
var permissionsComplete =
|
||||
// Microphone
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
) == PackageManager.PERMISSION_GRANTED &&
|
||||
// Camera
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED &&
|
||||
// Notifications
|
||||
NotificationManagerCompat.from(requireContext())
|
||||
.areNotificationsEnabled()
|
||||
) {
|
||||
// External Storage
|
||||
if (!BuildUtil.isGooglePlayBuild) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
permissionsComplete =
|
||||
(permissionsComplete && Environment.isExternalStorageManager())
|
||||
} else {
|
||||
permissionsComplete =
|
||||
(permissionsComplete && ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsComplete) {
|
||||
PageState.PAGE_STEPS_COMPLETE
|
||||
} else {
|
||||
PageState.PAGE_STEPS_INCOMPLETE
|
||||
|
|
@ -249,7 +320,7 @@ class SetupFragment : Fragment() {
|
|||
R.string.select_citra_user_folder_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
openCitraDirectory.launch(null)
|
||||
PermissionsHandler.compatibleSelectDirectory(openCitraDirectory)
|
||||
},
|
||||
buttonState = {
|
||||
if (PermissionsHandler.hasWriteAccess(requireContext())) {
|
||||
|
|
@ -452,6 +523,19 @@ class SetupFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showPermissionDeniedSnackbar() {
|
||||
Snackbar.make(binding.root, R.string.permission_denied, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.buttonNext)
|
||||
.setAction(R.string.grid_menu_core_settings) {
|
||||
val intent =
|
||||
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
val uri = Uri.fromParts("package", requireActivity().packageName, null)
|
||||
intent.data = uri
|
||||
startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private val permissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
|
|
@ -459,16 +543,20 @@ class SetupFragment : Fragment() {
|
|||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
Snackbar.make(binding.root, R.string.permission_denied, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(binding.buttonNext)
|
||||
.setAction(R.string.grid_menu_core_settings) {
|
||||
val intent =
|
||||
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
val uri = Uri.fromParts("package", requireActivity().packageName, null)
|
||||
intent.data = uri
|
||||
startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
showPermissionDeniedSnackbar()
|
||||
}
|
||||
|
||||
// We can't use permissionLauncher because MANAGE_EXTERNAL_STORAGE is a special snowflake
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private val manageExternalStoragePermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
checkForButtonState.invoke()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
showPermissionDeniedSnackbar()
|
||||
}
|
||||
|
||||
private val openCitraDirectory = registerForActivityResult<Uri, Uri>(
|
||||
|
|
@ -478,6 +566,17 @@ class SetupFragment : Fragment() {
|
|||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
if (!BuildUtil.isGooglePlayBuild) {
|
||||
if (NativeLibrary.getUserDirectory(result) == "") {
|
||||
SelectUserDirectoryDialogFragment.newInstance(
|
||||
mainActivity,
|
||||
R.string.invalid_selection,
|
||||
R.string.invalid_user_directory
|
||||
).show(mainActivity.supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
}
|
||||
|
||||
CitraDirectoryHelper(requireActivity(), true).showCitraDirectoryDialog(result, pageButtonCallback, checkForButtonState)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ class Game(
|
|||
val isInstalled: Boolean = false,
|
||||
val isSystemTitle: Boolean = false,
|
||||
val isVisibleSystemTitle: Boolean = false,
|
||||
val isInsertable: Boolean = false,
|
||||
val icon: IntArray? = null,
|
||||
val fileType: String = "",
|
||||
val isCompressed: Boolean = false,
|
||||
val filename: String,
|
||||
) : Parcelable {
|
||||
val keyAddedToLibraryTime get() = "${filename}_AddedToLibraryTime"
|
||||
|
|
@ -64,7 +66,7 @@ class Game(
|
|||
val allExtensions: Set<String> get() = extensions + badExtensions
|
||||
|
||||
val extensions: Set<String> = HashSet(
|
||||
listOf("3dsx", "elf", "axf", "cci", "cxi", "app")
|
||||
listOf("3dsx", "app", "axf", "cci", "cxi", "elf", "z3dsx", "zcci", "zcxi", "3ds")
|
||||
)
|
||||
|
||||
val badExtensions: Set<String> = HashSet(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ class GameInfo(path: String) {
|
|||
|
||||
external fun getFileType(): String
|
||||
|
||||
external fun getIsInsertable(): Boolean
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private external fun initialize(path: String): Long
|
||||
|
|
|
|||
|
|
@ -98,173 +98,199 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
|||
return onTouchWhileEditing(event)
|
||||
}
|
||||
|
||||
var hasActiveButtons = false
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
for (button in overlayButtons) {
|
||||
if (button.trackId == pointerId) {
|
||||
hasActiveButtons = true
|
||||
break
|
||||
}
|
||||
}
|
||||
var hasActiveDpad = false
|
||||
if (!hasActiveButtons) {
|
||||
for (dpad in overlayDpads) {
|
||||
if (dpad.trackId == pointerId) {
|
||||
hasActiveDpad = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasActiveJoystick = false
|
||||
if(!hasActiveButtons && !hasActiveDpad){
|
||||
for (joystick in overlayJoysticks) {
|
||||
if (joystick.trackId == pointerId) {
|
||||
hasActiveJoystick = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shouldUpdateView = false
|
||||
if(!hasActiveDpad && !hasActiveJoystick) {
|
||||
for (button in overlayButtons) {
|
||||
val stateChanged = button.updateStatus(event, hasActiveButtons, this)
|
||||
if (!stateChanged) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) {
|
||||
swapScreen()
|
||||
}
|
||||
else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) {
|
||||
TurboHelper.toggleTurbo(true)
|
||||
}
|
||||
else if (button.id == NativeLibrary.ButtonType.BUTTON_COMBO) {
|
||||
ComboHelper.comboActivate(button)
|
||||
}
|
||||
|
||||
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
button.id,
|
||||
button.status
|
||||
)
|
||||
|
||||
shouldUpdateView = true
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasActiveButtons && !hasActiveJoystick) {
|
||||
for (dpad in overlayDpads) {
|
||||
val stateChanged = dpad.updateStatus(
|
||||
event,
|
||||
hasActiveDpad,
|
||||
EmulationMenuSettings.dpadSlide,
|
||||
this
|
||||
)
|
||||
if (!stateChanged) {
|
||||
continue
|
||||
}
|
||||
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.upId,
|
||||
dpad.upStatus
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.downId,
|
||||
dpad.downStatus
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.leftId,
|
||||
dpad.leftStatus
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.rightId,
|
||||
dpad.rightStatus
|
||||
)
|
||||
|
||||
shouldUpdateView = true
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasActiveDpad && !hasActiveButtons) {
|
||||
for (joystick in overlayJoysticks) {
|
||||
val stateChanged = joystick.updateStatus(event, hasActiveJoystick, this)
|
||||
if (!stateChanged) {
|
||||
continue
|
||||
}
|
||||
|
||||
val axisID = joystick.joystickId
|
||||
NativeLibrary.onGamePadMoveEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
axisID,
|
||||
joystick.xAxis,
|
||||
joystick.yAxis
|
||||
)
|
||||
|
||||
shouldUpdateView = true
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdateView) {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
if (!preferences.getBoolean("isTouchEnabled", true)) {
|
||||
return true
|
||||
}
|
||||
|
||||
val xPosition = event.getX(pointerIndex).toInt()
|
||||
val yPosition = event.getY(pointerIndex).toInt()
|
||||
val motionEvent = event.action and MotionEvent.ACTION_MASK
|
||||
val isActionDown =
|
||||
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
|
||||
val isActionMove = motionEvent == MotionEvent.ACTION_MOVE
|
||||
val isActionUp =
|
||||
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
|
||||
if (isActionDown && !isTouchInputConsumed(pointerId)) {
|
||||
NativeLibrary.onTouchEvent(xPosition.toFloat(), yPosition.toFloat(), true)
|
||||
}
|
||||
if (isActionMove) {
|
||||
for (i in 0 until event.pointerCount) {
|
||||
val fingerId = event.getPointerId(i)
|
||||
if (isTouchInputConsumed(fingerId)) {
|
||||
continue
|
||||
|
||||
val pointerList = (0 until event.pointerCount).toMutableList()
|
||||
// Move the pointer that triggered the most recent event to the front
|
||||
// of the list so that it is processed first
|
||||
val currentActionPointer = event.actionIndex
|
||||
pointerList.remove(pointerList.indexOf(currentActionPointer))
|
||||
pointerList.add(0, currentActionPointer)
|
||||
|
||||
// Set up a loop for if we need to check touches other than the most recent one
|
||||
// (Only happens if we're dragging the touch)
|
||||
for (pointerIndex in pointerList) {
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
||||
val xPosition = event.getX(pointerIndex).toInt()
|
||||
val yPosition = event.getY(pointerIndex).toInt()
|
||||
|
||||
var hasActiveButtons = false
|
||||
for (button in overlayButtons) {
|
||||
if (button.trackId == pointerId) {
|
||||
hasActiveButtons = true
|
||||
break
|
||||
}
|
||||
NativeLibrary.onTouchMoved(xPosition.toFloat(), yPosition.toFloat())
|
||||
}
|
||||
}
|
||||
if (isActionUp && !isTouchInputConsumed(pointerId)) {
|
||||
NativeLibrary.onTouchEvent(0f, 0f, false)
|
||||
|
||||
var hasActiveDpad = false
|
||||
if (!hasActiveButtons) {
|
||||
for (dpad in overlayDpads) {
|
||||
if (dpad.trackId == pointerId) {
|
||||
hasActiveDpad = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasActiveJoystick = false
|
||||
if(!hasActiveButtons && !hasActiveDpad){
|
||||
for (joystick in overlayJoysticks) {
|
||||
if (joystick.trackId == pointerId) {
|
||||
hasActiveJoystick = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hasActiveOverlay = hasActiveButtons || hasActiveDpad || hasActiveJoystick
|
||||
|
||||
if (preferences.getBoolean("isTouchEnabled", true) && !hasActiveOverlay) {
|
||||
if (isActionMove) {
|
||||
NativeLibrary.onTouchMoved(xPosition.toFloat(), yPosition.toFloat())
|
||||
continue
|
||||
} else if (isActionUp) {
|
||||
NativeLibrary.onTouchEvent(0f, 0f, false)
|
||||
break // Up and down actions shouldn't loop
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var anyOverlayStateChanged = false
|
||||
var shouldUpdateView = false
|
||||
if(!hasActiveDpad && !hasActiveJoystick) {
|
||||
for (button in overlayButtons) {
|
||||
val stateChanged = button.updateStatus(event, pointerIndex, hasActiveButtons, this)
|
||||
if (!stateChanged) {
|
||||
continue
|
||||
}
|
||||
anyOverlayStateChanged = true
|
||||
|
||||
if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) {
|
||||
swapScreen()
|
||||
}
|
||||
else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) {
|
||||
TurboHelper.toggleTurbo(true)
|
||||
}
|
||||
else if (button.id == NativeLibrary.ButtonType.BUTTON_COMBO) {
|
||||
ComboHelper.comboActivate(button)
|
||||
}
|
||||
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
button.id,
|
||||
button.status
|
||||
)
|
||||
|
||||
shouldUpdateView = true
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasActiveButtons && !hasActiveJoystick) {
|
||||
for (dpad in overlayDpads) {
|
||||
val stateChanged = dpad.updateStatus(
|
||||
event,
|
||||
pointerIndex,
|
||||
hasActiveDpad,
|
||||
EmulationMenuSettings.dpadSlide,
|
||||
this
|
||||
)
|
||||
if (!stateChanged) {
|
||||
continue
|
||||
}
|
||||
anyOverlayStateChanged = true
|
||||
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.upId,
|
||||
dpad.upStatus
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.downId,
|
||||
dpad.downStatus
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.leftId,
|
||||
dpad.leftStatus
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
dpad.rightId,
|
||||
dpad.rightStatus
|
||||
)
|
||||
|
||||
shouldUpdateView = true
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasActiveDpad && !hasActiveButtons) {
|
||||
for (joystick in overlayJoysticks) {
|
||||
val stateChanged = joystick.updateStatus(event, pointerIndex, hasActiveJoystick, this)
|
||||
if (!stateChanged) {
|
||||
continue
|
||||
}
|
||||
anyOverlayStateChanged = true
|
||||
|
||||
val axisID = joystick.joystickId
|
||||
NativeLibrary.onGamePadMoveEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
axisID,
|
||||
joystick.xAxis,
|
||||
joystick.yAxis
|
||||
)
|
||||
|
||||
shouldUpdateView = true
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdateView) {
|
||||
invalidate()
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("isTouchEnabled", true) &&
|
||||
isActionDown &&
|
||||
!anyOverlayStateChanged
|
||||
) {
|
||||
// These need to be recalculated because touching the area
|
||||
// right in the middle of the dpad (between the "buttons") or
|
||||
// tapping a joystick in a certain way both don't cause
|
||||
// `anyOverlayStateChanged` to be set to true
|
||||
var isDpadPressed = false
|
||||
for (dpad in overlayDpads) {
|
||||
if (dpad.trackId == pointerId) {
|
||||
isDpadPressed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
var isJoystickPressed = false
|
||||
for (joystick in overlayJoysticks) {
|
||||
if (joystick.trackId == pointerId) {
|
||||
isJoystickPressed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDpadPressed && !isJoystickPressed) {
|
||||
NativeLibrary.onTouchEvent(xPosition.toFloat(), yPosition.toFloat(), true)
|
||||
}
|
||||
}
|
||||
|
||||
// We should only loop here if touch is being dragged
|
||||
if (!isActionMove) {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isTouchInputConsumed(trackId: Int): Boolean {
|
||||
overlayButtons.forEach {
|
||||
if (it.trackId == trackId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
overlayDpads.forEach {
|
||||
if (it.trackId == trackId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
overlayJoysticks.forEach {
|
||||
if (it.trackId == trackId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onTouchWhileEditing(event: MotionEvent): Boolean {
|
||||
val pointerIndex = event.actionIndex
|
||||
val fingerPositionX = event.getX(pointerIndex).toInt()
|
||||
|
|
|
|||
|
|
@ -70,9 +70,8 @@ class InputOverlayDrawableButton(
|
|||
*
|
||||
* @return true if value was changed
|
||||
*/
|
||||
fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean {
|
||||
fun updateStatus(event: MotionEvent, pointerIndex: Int, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean {
|
||||
val buttonSliding = EmulationMenuSettings.buttonSlide
|
||||
val pointerIndex = event.actionIndex
|
||||
val xPosition = event.getX(pointerIndex).toInt()
|
||||
val yPosition = event.getY(pointerIndex).toInt()
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
|
@ -92,7 +91,7 @@ class InputOverlayDrawableButton(
|
|||
if (trackId != pointerId) {
|
||||
return false
|
||||
}
|
||||
buttonUp(overlay)
|
||||
buttonUp(overlay, false)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -105,11 +104,14 @@ class InputOverlayDrawableButton(
|
|||
if (inside || trackId != pointerId) {
|
||||
return false
|
||||
}
|
||||
|
||||
// prevent the first (directly pressed) button to deactivate when sliding off
|
||||
if (buttonSliding == ButtonSlidingMode.Alternative.int && isMotionFirstButton) {
|
||||
return false
|
||||
}
|
||||
buttonUp(overlay)
|
||||
|
||||
val preserveTrackId = (buttonSliding != ButtonSlidingMode.Disabled.int)
|
||||
buttonUp(overlay, preserveTrackId)
|
||||
return true
|
||||
} else {
|
||||
// button was not yet pressed
|
||||
|
|
@ -132,10 +134,12 @@ class InputOverlayDrawableButton(
|
|||
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
private fun buttonUp(overlay: InputOverlay) {
|
||||
private fun buttonUp(overlay: InputOverlay, preserveTrackId: Boolean) {
|
||||
pressedState = false
|
||||
isMotionFirstButton = false
|
||||
trackId = -1
|
||||
if (!preserveTrackId) {
|
||||
trackId = -1
|
||||
}
|
||||
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,8 @@ class InputOverlayDrawableDpad(
|
|||
trackId = -1
|
||||
}
|
||||
|
||||
fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, dpadSlide: Boolean, overlay: InputOverlay): Boolean {
|
||||
fun updateStatus(event: MotionEvent, pointerIndex: Int, hasActiveButtons: Boolean, dpadSlide: Boolean, overlay: InputOverlay): Boolean {
|
||||
var isDown = false
|
||||
val pointerIndex = event.actionIndex
|
||||
val xPosition = event.getX(pointerIndex).toInt()
|
||||
val yPosition = event.getY(pointerIndex).toInt()
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
|
|
|||
|
|
@ -93,8 +93,7 @@ class InputOverlayDrawableJoystick(
|
|||
currentStateBitmapDrawable.draw(canvas)
|
||||
}
|
||||
|
||||
fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean {
|
||||
val pointerIndex = event.actionIndex
|
||||
fun updateStatus(event: MotionEvent, pointerIndex: Int, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean {
|
||||
val xPosition = event.getX(pointerIndex).toInt()
|
||||
val yPosition = event.getY(pointerIndex).toInt()
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
|
||||
package org.citra.citra_emu.ui.main
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.view.WindowManager
|
||||
|
|
@ -36,6 +40,8 @@ import androidx.work.WorkManager
|
|||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.BuildConfig
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||
import org.citra.citra_emu.databinding.ActivityMainBinding
|
||||
|
|
@ -43,14 +49,17 @@ import org.citra.citra_emu.features.settings.model.Settings
|
|||
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.GrantMissingFilesystemPermissionFragment
|
||||
import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment
|
||||
import org.citra.citra_emu.fragments.UpdateUserDirectoryDialogFragment
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
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
|
||||
import org.citra.citra_emu.utils.RefreshRateUtil
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
|
@ -66,6 +75,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
override var themeId: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
RefreshRateUtil.enforceRefreshRate(this)
|
||||
|
||||
val splashScreen = installSplashScreen()
|
||||
CitraDirectoryUtils.attemptAutomaticUpdateDirectory()
|
||||
splashScreen.setKeepOnScreenCondition {
|
||||
|
|
@ -185,14 +196,53 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||
|
||||
if (!firstTimeSetup && !PermissionsHandler.hasWriteAccess(this) &&
|
||||
!homeViewModel.isPickingUserDir.value
|
||||
) {
|
||||
if (firstTimeSetup) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!BuildUtil.isGooglePlayBuild) {
|
||||
fun requestMissingFilesystemPermission() =
|
||||
GrantMissingFilesystemPermissionFragment.newInstance()
|
||||
.show(supportFragmentManager, GrantMissingFilesystemPermissionFragment.TAG)
|
||||
|
||||
if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
requestMissingFilesystemPermission()
|
||||
}
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestMissingFilesystemPermission()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (homeViewModel.isPickingUserDir.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!PermissionsHandler.hasWriteAccess(this)) {
|
||||
SelectUserDirectoryDialogFragment.newInstance(this)
|
||||
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||
} else if (!firstTimeSetup && !homeViewModel.isPickingUserDir.value && CitraDirectoryUtils.needToUpdateManually()) {
|
||||
return
|
||||
} else if (CitraDirectoryUtils.needToUpdateManually()) {
|
||||
UpdateUserDirectoryDialogFragment.newInstance(this)
|
||||
.show(supportFragmentManager,UpdateUserDirectoryDialogFragment.TAG)
|
||||
return
|
||||
}
|
||||
|
||||
if (!BuildUtil.isGooglePlayBuild) {
|
||||
if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) {
|
||||
if (NativeLibrary.getUserDirectory() == "") {
|
||||
SelectUserDirectoryDialogFragment.newInstance(this)
|
||||
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,6 +366,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
if (!BuildUtil.isGooglePlayBuild) {
|
||||
if (NativeLibrary.getUserDirectory(result) == "") {
|
||||
SelectUserDirectoryDialogFragment.newInstance(
|
||||
this,
|
||||
R.string.invalid_selection,
|
||||
R.string.invalid_user_directory
|
||||
).show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||
return@registerForActivityResult
|
||||
}
|
||||
}
|
||||
|
||||
CitraDirectoryHelper(this@MainActivity, permissionsLost)
|
||||
.showCitraDirectoryDialog(result, buttonState = {})
|
||||
}
|
||||
|
|
@ -332,7 +393,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
}
|
||||
|
||||
val selectedFiles =
|
||||
FileBrowserHelper.getSelectedFiles(result, applicationContext, listOf("cia"))
|
||||
FileBrowserHelper.getSelectedFiles(result, applicationContext, listOf("cia", "zcia"))
|
||||
if (selectedFiles == null) {
|
||||
Toast.makeText(applicationContext, R.string.cia_file_not_found, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
// 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 org.citra.citra_emu.BuildConfig
|
||||
|
||||
object BuildUtil {
|
||||
@Suppress("unused")
|
||||
object BuildFlavors {
|
||||
const val GOOGLEPLAY = "googlePlay"
|
||||
const val VANILLA = "vanilla"
|
||||
}
|
||||
|
||||
fun assertNotGooglePlay() {
|
||||
if (isGooglePlayBuild) {
|
||||
error("Non-GooglePlay code being called in GooglePlay build")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SimplifyBooleanWithConstants", "KotlinConstantConditions")
|
||||
val isGooglePlayBuild =
|
||||
BuildConfig.FLAVOR == BuildFlavors.GOOGLEPLAY
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -28,8 +28,8 @@ object DirectoryInitialization {
|
|||
@Volatile
|
||||
private var directoryState: DirectoryInitializationState? = null
|
||||
var userPath: String? = null
|
||||
val internalUserPath
|
||||
get() = CitraApplication.appContext.getExternalFilesDir(null)!!.canonicalPath
|
||||
val internalUserPath: String
|
||||
get() = CitraApplication.appContext.filesDir.canonicalPath
|
||||
private val isCitraDirectoryInitializationRunning = AtomicBoolean(false)
|
||||
|
||||
val context: Context get() = CitraApplication.appContext
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ object DiskShaderCacheProgress {
|
|||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadProgress(stage: LoadCallbackStage, progress: Int, max: Int) {
|
||||
fun loadProgress(stage: LoadCallbackStage, progress: Int, max: Int, obj: String) {
|
||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
|
||||
|
|
@ -40,7 +40,7 @@ object DiskShaderCacheProgress {
|
|||
)
|
||||
|
||||
LoadCallbackStage.Build -> emulationViewModel.updateProgress(
|
||||
emulationActivity.getString(R.string.building_shaders),
|
||||
emulationActivity.getString(R.string.building_shaders, obj ),
|
||||
progress,
|
||||
max
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ package org.citra.citra_emu.utils
|
|||
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.model.CheapDocument
|
||||
import java.net.URLDecoder
|
||||
import java.nio.file.Paths
|
||||
import java.util.StringTokenizer
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
|
@ -191,7 +193,7 @@ class DocumentsTree {
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
fun renameFile(filepath: String, destinationFilename: String?): Boolean {
|
||||
fun renameFile(filepath: String, destinationFilename: String): Boolean {
|
||||
val node = resolvePath(filepath) ?: return false
|
||||
try {
|
||||
val filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD)
|
||||
|
|
@ -203,6 +205,20 @@ class DocumentsTree {
|
|||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun moveFile(filename: String, sourceDirPath: String, destDirPath: String): Boolean {
|
||||
val sourceFileNode = resolvePath(sourceDirPath + "/" + filename) ?: return false
|
||||
val sourceDirNode = resolvePath(sourceDirPath) ?: return false
|
||||
val destDirNode = resolvePath(destDirPath) ?: return false
|
||||
try {
|
||||
val newUri = DocumentsContract.moveDocument(context.contentResolver, sourceFileNode.uri!!, sourceDirNode.uri!!, destDirNode.uri!!)
|
||||
updateDocumentLocation("$sourceDirPath/$filename", "$destDirPath/$filename")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
error("[DocumentsTree]: Cannot move file, error: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteDocument(filepath: String): Boolean {
|
||||
val node = resolvePath(filepath) ?: return false
|
||||
|
|
@ -219,6 +235,29 @@ class DocumentsTree {
|
|||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean {
|
||||
val sourceNode = resolvePath(sourcePath)
|
||||
val newName = Paths.get(destinationPath).fileName.toString()
|
||||
val parentPath = Paths.get(destinationPath).parent.toString()
|
||||
val newParent = resolvePath(parentPath)
|
||||
val newUri = (getUri(parentPath).toString() + "%2F$newName").toUri() // <- Is there a better way?
|
||||
|
||||
if (sourceNode == null || newParent == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
sourceNode.parent!!.removeChild(sourceNode)
|
||||
|
||||
sourceNode.name = newName
|
||||
sourceNode.parent = newParent
|
||||
sourceNode.uri = newUri
|
||||
|
||||
newParent.addChild(sourceNode)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun resolvePath(filepath: String): DocumentsNode? {
|
||||
root ?: return 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.
|
||||
|
||||
|
|
@ -18,15 +18,27 @@ object EmulationLifecycleUtil {
|
|||
}
|
||||
|
||||
fun addShutdownHook(hook: Runnable) {
|
||||
shutdownHooks.add(hook)
|
||||
if (shutdownHooks.contains(hook)) {
|
||||
Log.warning("[EmulationLifecycleUtil] Tried to add shutdown hook for function that already existed. Skipping.")
|
||||
} else {
|
||||
shutdownHooks.add(hook)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPauseResumeHook(hook: Runnable) {
|
||||
pauseResumeHooks.add(hook)
|
||||
if (pauseResumeHooks.contains(hook)) {
|
||||
Log.warning("[EmulationLifecycleUtil] Tried to add pause resume hook for function that already existed. Skipping.")
|
||||
} else {
|
||||
pauseResumeHooks.add(hook)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
pauseResumeHooks.clear()
|
||||
shutdownHooks.clear()
|
||||
fun removeHook(hook: Runnable) {
|
||||
if (pauseResumeHooks.contains(hook)) {
|
||||
pauseResumeHooks.remove(hook)
|
||||
}
|
||||
if (shutdownHooks.contains(hook)) {
|
||||
shutdownHooks.remove(hook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue