Actually renamed Android module
This commit is contained in:
parent
13e79aa43f
commit
edd7896018
|
@ -94,8 +94,8 @@ if (ANDROID AND SUDACHI_DOWNLOAD_ANDROID_VVL)
|
||||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
|
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/externals")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Copy the arm64 binary to src/android/app/main/jniLibs
|
# Copy the arm64 binary to src/android/sudachi/main/jniLibs
|
||||||
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/app/src/main/jniLibs/arm64-v8a/")
|
set(vvl_lib_path "${CMAKE_CURRENT_SOURCE_DIR}/src/android/sudachi/src/main/jniLibs/arm64-v8a/")
|
||||||
file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so"
|
file(COPY "${CMAKE_BINARY_DIR}/externals/android-binaries-${vvl_version}/arm64-v8a/libVkLayer_khronos_validation.so"
|
||||||
DESTINATION "${vvl_lib_path}")
|
DESTINATION "${vvl_lib_path}")
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -8,7 +8,7 @@ source_lang = en
|
||||||
type = QT
|
type = QT
|
||||||
|
|
||||||
[o:sudachi-emulator:p:sudachi:r:sudachi-android]
|
[o:sudachi-emulator:p:sudachi:r:sudachi-android]
|
||||||
file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml
|
file_filter = ../../src/android/sudachi/src/main/res/values-<lang>/strings.xml
|
||||||
source_file = ../../src/android/app/src/main/res/values/strings.xml
|
source_file = ../../src/android/sudachi/src/main/res/values/strings.xml
|
||||||
type = ANDROID
|
type = ANDROID
|
||||||
lang_map = ja_JP:ja, ko_KR:ko, pt_BR:pt-rBR, pt_PT:pt-rPT, ru_RU:ru, vi_VN:vi, zh_CN:zh-rCN, zh_TW:zh-rTW
|
lang_map = ja_JP:ja, ko_KR:ko, pt_BR:pt-rBR, pt_PT:pt-rPT, ru_RU:ru, vi_VN:vi, zh_CN:zh-rCN, zh_TW:zh-rTW
|
||||||
|
|
|
@ -214,6 +214,6 @@ if (ENABLE_WEB_SERVICE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (ANDROID)
|
if (ANDROID)
|
||||||
add_subdirectory(android/app/src/main/jni)
|
add_subdirectory(android/sudachi/src/main/jni)
|
||||||
target_include_directories(sudachi-android PRIVATE android/app/src/main)
|
target_include_directories(sudachi-android PRIVATE android/sudachi/src/main)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -47,7 +47,7 @@ captures/
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|
||||||
# CXX compile cache
|
# CXX compile cache
|
||||||
app/.cxx
|
sudachi/.cxx
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
# Google Services (e.g. APIs or Firebase)
|
||||||
google-services.json
|
google-services.json
|
||||||
|
@ -67,5 +67,5 @@ fastlane/readme.md
|
||||||
# Autogenerated library for vulkan validation layers
|
# Autogenerated library for vulkan validation layers
|
||||||
libVkLayer_khronos_validation.so
|
libVkLayer_khronos_validation.so
|
||||||
|
|
||||||
app/ea
|
sudachi/ea
|
||||||
app/mainline
|
sudachi/mainline
|
|
@ -1,24 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
# To get usable stack traces
|
|
||||||
-dontobfuscate
|
|
||||||
|
|
||||||
# Prevents crashing when using Wini
|
|
||||||
-keep class org.ini4j.spi.IniParser
|
|
||||||
-keep class org.ini4j.spi.IniBuilder
|
|
||||||
-keep class org.ini4j.spi.IniFormatter
|
|
||||||
|
|
||||||
# Suppress warnings for R8
|
|
||||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
|
||||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
|
||||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
|
||||||
-dontwarn org.conscrypt.Conscrypt$Version
|
|
||||||
-dontwarn org.conscrypt.Conscrypt
|
|
||||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
|
||||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
|
||||||
-dontwarn java.beans.Introspector
|
|
||||||
-dontwarn java.beans.VetoableChangeListener
|
|
||||||
-dontwarn java.beans.VetoableChangeSupport
|
|
|
@ -1,22 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="200dp"
|
|
||||||
android:height="200dp"
|
|
||||||
android:viewportWidth="500"
|
|
||||||
android:viewportHeight="500">
|
|
||||||
<path
|
|
||||||
android:fillColor="#C6C6C6"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M262.66,175.11L262.66,375.05C318.54,375.05 363.85,330.29 363.85,275.08C363.85,219.87 318.54,175.11 262.66,175.11M282.43,197.01C318.67,206 344.09,238.19 344.09,275.11C344.09,312.03 318.67,344.22 282.43,353.2L282.43,197.01"
|
|
||||||
android:strokeWidth="1.46"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt"
|
|
||||||
android:strokeLineJoin="miter" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFDC00"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M237.31,125.11C181.43,125.11 136.12,169.87 136.12,225.08C136.12,280.29 181.43,325.05 237.31,325.05ZM217.57,147.01L217.57,303.2C189.11,296.16 166.67,274.54 158.84,246.6C151.01,218.65 159,188.71 179.75,168.21C190.16,157.86 203.24,150.53 217.57,147.01"
|
|
||||||
android:strokeWidth="1.46"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeLineCap="butt"
|
|
||||||
android:strokeLineJoin="miter" />
|
|
||||||
</vector>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="155.3dp"
|
|
||||||
android:height="172.55dp"
|
|
||||||
android:viewportWidth="155.3"
|
|
||||||
android:viewportHeight="172.55">
|
|
||||||
<path
|
|
||||||
android:fillColor="#C6C6C6"
|
|
||||||
android:pathData="M86.28,34.51v138a69,69 0,0 0,0 -138M99.76,49.63a55.57,55.57 0,0 1,0 107.8V49.63" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFDC00"
|
|
||||||
android:pathData="M69,0a69,69 0,0 0,0 138ZM55.54,15.12v107.8A55.55,55.55 0,0 1,29.75 29.75,55.1 55.1,0 0,1 55.54,15.12" />
|
|
||||||
</vector>
|
|
|
@ -1,24 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="340.97dp"
|
|
||||||
android:height="389.85dp"
|
|
||||||
android:viewportWidth="340.97"
|
|
||||||
android:viewportHeight="389.85">
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurface"
|
|
||||||
android:pathData="M341,268.68v73c0,14.5 -2.24,25.24 -6.83,32.82 -5.92,10.15 -16.21,15.32 -30.54,15.32S279,384.61 273,374.27c-4.56,-7.64 -6.8,-18.42 -6.8,-32.92V268.68a4.52,4.52 0,0 1,4.51 -4.51H273a4.5,4.5 0,0 1,4.5 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.52,4.52 0,0 1,4.52 -4.51h2.27A4.5,4.5 0,0 1,341 268.68Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurface"
|
|
||||||
android:pathData="M246.49,389.85H178.6c-2.35,0 -4.72,-1.88 -4.72,-6.08a8.28,8.28 0,0 1,1.33 -4.48l60.33,-104.47H186a4.51,4.51 0,0 1,-4.51 -4.51v-1.58a4.51,4.51 0,0 1,4.48 -4.51h0.8c58.69,-0.11 59.12,0 59.67,0.07a5.19,5.19 0,0 1,4 5.8,8.69 8.69,0 0,1 -1.33,3.76l-60.6,104.77h58a4.51,4.51 0,0 1,4.51 4.51v2.21A4.51,4.51 0,0 1,246.49 389.85Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurface"
|
|
||||||
android:pathData="M73.6,268.68v82.06c0,26 -11.8,38.44 -37.12,39.09h-0.12a4.51,4.51 0,0 1,-4.51 -4.51V383a4.51,4.51 0,0 1,4.48 -4.5c18.49,-0.15 26,-8.23 26,-27.9v-2.37A32.34,32.34 0,0 1,59 351.46c-6.39,5.5 -14.5,8.29 -24.07,8.29C12.09,359.75 0,347.34 0,323.86V268.68a4.52,4.52 0,0 1,4.51 -4.51H6.73a4.52,4.52 0,0 1,4.5 4.51v55c0,7.6 1.82,14.22 5,18.18 3.57,4.56 9.17,6.49 18.75,6.49 10.13,0 17.32,-3.76 22,-11.5 3.61,-5.92 5.43,-13.66 5.43,-23V268.68a4.52,4.52 0,0 1,4.51 -4.51h2.22A4.52,4.52 0,0 1,73.6 268.68Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnSurface"
|
|
||||||
android:pathData="M163.27,268.68v73c0,14.5 -2.24,25.24 -6.84,32.82 -5.92,10.15 -16.2,15.32 -30.53,15.32s-24.62,-5.23 -30.58,-15.57c-4.56,-7.64 -6.79,-18.42 -6.79,-32.92V268.68A4.51,4.51 0,0 1,93 264.17h2.28a4.51,4.51 0,0 1,4.51 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.51,4.51 0,0 1,4.51 -4.51h2.27A4.51,4.51 0,0 1,163.27 268.68Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#C6C6C6"
|
|
||||||
android:pathData="M181.2,42.83V214.17a85.67,85.67 0,0 0,0 -171.34M197.93,61.6a69,69 0,0 1,0 133.8V61.6" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFDC00"
|
|
||||||
android:pathData="M159.78,0a85.67,85.67 0,1 0,0 171.33ZM143.05,18.77v133.8A69,69 0,0 1,111 36.92a68.47,68.47 0,0 1,32 -18.15" />
|
|
||||||
</vector>
|
|
|
@ -1,95 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
-->
|
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
|
||||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
|
||||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name="org.sudachi.sudachi_emu.SudachiApplication"
|
|
||||||
android:label="@string/app_name_suffixed"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:hasFragileUserData="false"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:isGame="true"
|
|
||||||
android:appCategory="game"
|
|
||||||
android:banner="@drawable/tv_banner"
|
|
||||||
android:fullBackupContent="@xml/data_extraction_rules"
|
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules_api_31"
|
|
||||||
android:enableOnBackInvokedCallback="true">
|
|
||||||
|
|
||||||
<meta-data android:name="android.game_mode_config"
|
|
||||||
android:resource="@xml/game_mode_config" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="org.sudachi.sudachi_emu.ui.main.MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@style/Theme.Sudachi.Splash.Main">
|
|
||||||
|
|
||||||
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="org.sudachi.sudachi_emu.features.settings.ui.SettingsActivity"
|
|
||||||
android:theme="@style/Theme.Sudachi.Main"
|
|
||||||
android:label="@string/preferences_settings"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="org.sudachi.sudachi_emu.activities.EmulationActivity"
|
|
||||||
android:theme="@style/Theme.Sudachi.Main"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:supportsPictureInPicture="true"
|
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
|
||||||
android:exported="true">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.nfc.action.TECH_DISCOVERED" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="application/octet-stream" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data
|
|
||||||
android:mimeType="application/octet-stream"
|
|
||||||
android:scheme="content"/>
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.nfc.action.TECH_DISCOVERED"
|
|
||||||
android:resource="@xml/nfc_tech_filter" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name=".features.DocumentProvider"
|
|
||||||
android:authorities="${applicationId}.user"
|
|
||||||
android:grantUriPermissions="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
|
||||||
</intent-filter>
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
|
@ -1,55 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import java.io.File
|
|
||||||
import org.sudachi.sudachi_emu.utils.DirectoryInitialization
|
|
||||||
import org.sudachi.sudachi_emu.utils.DocumentsTree
|
|
||||||
import org.sudachi.sudachi_emu.utils.GpuDriverHelper
|
|
||||||
import org.sudachi.sudachi_emu.utils.Log
|
|
||||||
|
|
||||||
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
|
|
||||||
|
|
||||||
class SudachiApplication : Application() {
|
|
||||||
private fun createNotificationChannels() {
|
|
||||||
val noticeChannel = NotificationChannel(
|
|
||||||
getString(R.string.notice_notification_channel_id),
|
|
||||||
getString(R.string.notice_notification_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
)
|
|
||||||
noticeChannel.description = getString(R.string.notice_notification_channel_description)
|
|
||||||
noticeChannel.setSound(null, null)
|
|
||||||
|
|
||||||
// Register the channel with the system; you can't change the importance
|
|
||||||
// or other notification behaviors after this
|
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
||||||
notificationManager.createNotificationChannel(noticeChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
application = this
|
|
||||||
documentsTree = DocumentsTree()
|
|
||||||
DirectoryInitialization.start()
|
|
||||||
GpuDriverHelper.initializeDriverParameters()
|
|
||||||
NativeInput.reloadInputDevices()
|
|
||||||
NativeLibrary.logDeviceInfo()
|
|
||||||
Log.logDeviceInfo()
|
|
||||||
|
|
||||||
createNotificationChannels()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var documentsTree: DocumentsTree? = null
|
|
||||||
lateinit var application: SudachiApplication
|
|
||||||
|
|
||||||
val appContext: Context
|
|
||||||
get() = application.applicationContext
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,509 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.activities
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.PictureInPictureParams
|
|
||||||
import android.app.RemoteAction
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.Icon
|
|
||||||
import android.hardware.Sensor
|
|
||||||
import android.hardware.SensorEvent
|
|
||||||
import android.hardware.SensorEventListener
|
|
||||||
import android.hardware.SensorManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Rational
|
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.Surface
|
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ActivityEmulationBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.BooleanSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.IntSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.Settings
|
|
||||||
import org.sudachi.sudachi_emu.model.EmulationViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.Game
|
|
||||||
import org.sudachi.sudachi_emu.utils.InputHandler
|
|
||||||
import org.sudachi.sudachi_emu.utils.Log
|
|
||||||
import org.sudachi.sudachi_emu.utils.MemoryUtil
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
import org.sudachi.sudachi_emu.utils.NfcReader
|
|
||||||
import org.sudachi.sudachi_emu.utils.ParamPackage
|
|
||||||
import org.sudachi.sudachi_emu.utils.ThemeHelper
|
|
||||||
import java.text.NumberFormat
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|
||||||
private lateinit var binding: ActivityEmulationBinding
|
|
||||||
|
|
||||||
var isActivityRecreated = false
|
|
||||||
private lateinit var nfcReader: NfcReader
|
|
||||||
|
|
||||||
private val gyro = FloatArray(3)
|
|
||||||
private val accel = FloatArray(3)
|
|
||||||
private var motionTimestamp: Long = 0
|
|
||||||
private var flipMotionOrientation: Boolean = false
|
|
||||||
|
|
||||||
private val actionPause = "ACTION_EMULATOR_PAUSE"
|
|
||||||
private val actionPlay = "ACTION_EMULATOR_PLAY"
|
|
||||||
private val actionMute = "ACTION_EMULATOR_MUTE"
|
|
||||||
private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
|
|
||||||
|
|
||||||
private val emulationViewModel: EmulationViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
Log.gameLaunched = true
|
|
||||||
ThemeHelper.setTheme(this)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
InputHandler.updateControllerData()
|
|
||||||
val players = NativeConfig.getInputSettings(true)
|
|
||||||
var hasConfiguredControllers = false
|
|
||||||
players.forEach {
|
|
||||||
if (it.hasMapping()) {
|
|
||||||
hasConfiguredControllers = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasConfiguredControllers && InputHandler.androidControllers.isNotEmpty()) {
|
|
||||||
var params: ParamPackage? = null
|
|
||||||
for (controller in InputHandler.registeredControllers) {
|
|
||||||
if (controller.get("port", -1) == 0) {
|
|
||||||
params = controller
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params != null) {
|
|
||||||
NativeInput.updateMappingsWithDefault(
|
|
||||||
0,
|
|
||||||
params,
|
|
||||||
params.get("display", getString(R.string.unknown))
|
|
||||||
)
|
|
||||||
NativeConfig.saveGlobalConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding = ActivityEmulationBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
val navHostFragment =
|
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
|
||||||
navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras)
|
|
||||||
|
|
||||||
isActivityRecreated = savedInstanceState != null
|
|
||||||
|
|
||||||
// Set these options now so that the SurfaceView the game renders into is the right size.
|
|
||||||
enableFullscreenImmersive()
|
|
||||||
|
|
||||||
window.decorView.setBackgroundColor(getColor(android.R.color.black))
|
|
||||||
|
|
||||||
nfcReader = NfcReader(this)
|
|
||||||
nfcReader.initialize()
|
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(SudachiApplication.appContext)
|
|
||||||
if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
|
|
||||||
if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) {
|
|
||||||
Toast.makeText(
|
|
||||||
this,
|
|
||||||
getString(
|
|
||||||
R.string.device_memory_inadequate,
|
|
||||||
MemoryUtil.getDeviceRAM(),
|
|
||||||
getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
NumberFormat.getInstance().format(MemoryUtil.REQUIRED_MEMORY),
|
|
||||||
getString(R.string.memory_gigabyte)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, true)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
||||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
||||||
// Special case, we do not support multiline input, dismiss the keyboard.
|
|
||||||
val overlayView: View =
|
|
||||||
this.findViewById(R.id.surface_input_overlay)
|
|
||||||
val im =
|
|
||||||
overlayView.context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
|
||||||
im.hideSoftInputFromWindow(overlayView.windowToken, 0)
|
|
||||||
} else {
|
|
||||||
val textChar = event.unicodeChar
|
|
||||||
if (textChar == 0) {
|
|
||||||
// No text, button input.
|
|
||||||
NativeLibrary.submitInlineKeyboardInput(keyCode)
|
|
||||||
} else {
|
|
||||||
// Text submitted.
|
|
||||||
NativeLibrary.submitInlineKeyboardText(textChar.toChar().toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
nfcReader.startScanning()
|
|
||||||
startMotionSensorListener()
|
|
||||||
InputHandler.updateControllerData()
|
|
||||||
|
|
||||||
buildPictureInPictureParams()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
nfcReader.stopScanning()
|
|
||||||
stopMotionSensorListener()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUserLeaveHint() {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
||||||
if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
|
|
||||||
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
|
|
||||||
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
|
|
||||||
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
setIntent(intent)
|
|
||||||
nfcReader.onNewIntent(intent)
|
|
||||||
InputHandler.updateControllerData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
|
|
||||||
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
|
|
||||||
) {
|
|
||||||
return super.dispatchKeyEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emulationViewModel.drawerOpen.value) {
|
|
||||||
return super.dispatchKeyEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
return InputHandler.dispatchKeyEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
|
|
||||||
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
|
|
||||||
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
|
|
||||||
) {
|
|
||||||
return super.dispatchGenericMotionEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emulationViewModel.drawerOpen.value) {
|
|
||||||
return super.dispatchGenericMotionEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't attempt to do anything if we are disconnecting a device.
|
|
||||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return InputHandler.dispatchGenericMotionEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSensorChanged(event: SensorEvent) {
|
|
||||||
val rotation = this.display?.rotation
|
|
||||||
if (rotation == Surface.ROTATION_90) {
|
|
||||||
flipMotionOrientation = true
|
|
||||||
}
|
|
||||||
if (rotation == Surface.ROTATION_270) {
|
|
||||||
flipMotionOrientation = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
|
|
||||||
if (flipMotionOrientation) {
|
|
||||||
accel[0] = event.values[1] / SensorManager.GRAVITY_EARTH
|
|
||||||
accel[1] = -event.values[0] / SensorManager.GRAVITY_EARTH
|
|
||||||
} else {
|
|
||||||
accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH
|
|
||||||
accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH
|
|
||||||
}
|
|
||||||
accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH
|
|
||||||
}
|
|
||||||
if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {
|
|
||||||
// Investigate why sensor value is off by 6x
|
|
||||||
if (flipMotionOrientation) {
|
|
||||||
gyro[0] = -event.values[1] / 6.0f
|
|
||||||
gyro[1] = event.values[0] / 6.0f
|
|
||||||
} else {
|
|
||||||
gyro[0] = event.values[1] / 6.0f
|
|
||||||
gyro[1] = -event.values[0] / 6.0f
|
|
||||||
}
|
|
||||||
gyro[2] = event.values[2] / 6.0f
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update state on accelerometer data
|
|
||||||
if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
|
|
||||||
motionTimestamp = event.timestamp
|
|
||||||
NativeInput.onDeviceMotionEvent(
|
|
||||||
NativeInput.Player1Device,
|
|
||||||
deltaTimestamp,
|
|
||||||
gyro[0],
|
|
||||||
gyro[1],
|
|
||||||
gyro[2],
|
|
||||||
accel[0],
|
|
||||||
accel[1],
|
|
||||||
accel[2]
|
|
||||||
)
|
|
||||||
NativeInput.onDeviceMotionEvent(
|
|
||||||
NativeInput.ConsoleDevice,
|
|
||||||
deltaTimestamp,
|
|
||||||
gyro[0],
|
|
||||||
gyro[1],
|
|
||||||
gyro[2],
|
|
||||||
accel[0],
|
|
||||||
accel[1],
|
|
||||||
accel[2]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
|
|
||||||
|
|
||||||
private fun enableFullscreenImmersive() {
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
|
|
||||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
|
||||||
controller.systemBarsBehavior =
|
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
|
|
||||||
PictureInPictureParams.Builder {
|
|
||||||
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
|
|
||||||
0 -> Rational(16, 9)
|
|
||||||
1 -> Rational(4, 3)
|
|
||||||
2 -> Rational(21, 9)
|
|
||||||
3 -> Rational(16, 10)
|
|
||||||
else -> null // Best fit
|
|
||||||
}
|
|
||||||
return this.apply { aspectRatio?.let { setAspectRatio(it) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder():
|
|
||||||
PictureInPictureParams.Builder {
|
|
||||||
val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
|
|
||||||
val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
|
|
||||||
if (NativeLibrary.isPaused()) {
|
|
||||||
val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
|
|
||||||
val playPendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this@EmulationActivity,
|
|
||||||
R.drawable.ic_pip_play,
|
|
||||||
Intent(actionPlay),
|
|
||||||
pendingFlags
|
|
||||||
)
|
|
||||||
val playRemoteAction = RemoteAction(
|
|
||||||
playIcon,
|
|
||||||
getString(R.string.play),
|
|
||||||
getString(R.string.play),
|
|
||||||
playPendingIntent
|
|
||||||
)
|
|
||||||
pictureInPictureActions.add(playRemoteAction)
|
|
||||||
} else {
|
|
||||||
val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
|
|
||||||
val pausePendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this@EmulationActivity,
|
|
||||||
R.drawable.ic_pip_pause,
|
|
||||||
Intent(actionPause),
|
|
||||||
pendingFlags
|
|
||||||
)
|
|
||||||
val pauseRemoteAction = RemoteAction(
|
|
||||||
pauseIcon,
|
|
||||||
getString(R.string.pause),
|
|
||||||
getString(R.string.pause),
|
|
||||||
pausePendingIntent
|
|
||||||
)
|
|
||||||
pictureInPictureActions.add(pauseRemoteAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
|
||||||
val unmuteIcon = Icon.createWithResource(
|
|
||||||
this@EmulationActivity,
|
|
||||||
R.drawable.ic_pip_unmute
|
|
||||||
)
|
|
||||||
val unmutePendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this@EmulationActivity,
|
|
||||||
R.drawable.ic_pip_unmute,
|
|
||||||
Intent(actionUnmute),
|
|
||||||
pendingFlags
|
|
||||||
)
|
|
||||||
val unmuteRemoteAction = RemoteAction(
|
|
||||||
unmuteIcon,
|
|
||||||
getString(R.string.unmute),
|
|
||||||
getString(R.string.unmute),
|
|
||||||
unmutePendingIntent
|
|
||||||
)
|
|
||||||
pictureInPictureActions.add(unmuteRemoteAction)
|
|
||||||
} else {
|
|
||||||
val muteIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_mute)
|
|
||||||
val mutePendingIntent = PendingIntent.getBroadcast(
|
|
||||||
this@EmulationActivity,
|
|
||||||
R.drawable.ic_pip_mute,
|
|
||||||
Intent(actionMute),
|
|
||||||
pendingFlags
|
|
||||||
)
|
|
||||||
val muteRemoteAction = RemoteAction(
|
|
||||||
muteIcon,
|
|
||||||
getString(R.string.mute),
|
|
||||||
getString(R.string.mute),
|
|
||||||
mutePendingIntent
|
|
||||||
)
|
|
||||||
pictureInPictureActions.add(muteRemoteAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.apply { setActions(pictureInPictureActions) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildPictureInPictureParams() {
|
|
||||||
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
|
|
||||||
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val isEmulationActive = emulationViewModel.emulationStarted.value &&
|
|
||||||
!emulationViewModel.isEmulationStopping.value
|
|
||||||
pictureInPictureParamsBuilder.setAutoEnterEnabled(
|
|
||||||
BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
private var pictureInPictureReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent) {
|
|
||||||
if (intent.action == actionPlay) {
|
|
||||||
if (NativeLibrary.isPaused()) NativeLibrary.unpauseEmulation()
|
|
||||||
} else if (intent.action == actionPause) {
|
|
||||||
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
|
|
||||||
}
|
|
||||||
if (intent.action == actionUnmute) {
|
|
||||||
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
|
||||||
BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
|
||||||
}
|
|
||||||
} else if (intent.action == actionMute) {
|
|
||||||
if (!BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
|
||||||
BooleanSetting.AUDIO_MUTED.setBoolean(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildPictureInPictureParams()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
|
||||||
override fun onPictureInPictureModeChanged(
|
|
||||||
isInPictureInPictureMode: Boolean,
|
|
||||||
newConfig: Configuration
|
|
||||||
) {
|
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
|
||||||
if (isInPictureInPictureMode) {
|
|
||||||
IntentFilter().apply {
|
|
||||||
addAction(actionPause)
|
|
||||||
addAction(actionPlay)
|
|
||||||
addAction(actionMute)
|
|
||||||
addAction(actionUnmute)
|
|
||||||
}.also {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
registerReceiver(pictureInPictureReceiver, it, RECEIVER_EXPORTED)
|
|
||||||
} else {
|
|
||||||
registerReceiver(pictureInPictureReceiver, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
unregisterReceiver(pictureInPictureReceiver)
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
}
|
|
||||||
// Always resume audio, since there is no UI button
|
|
||||||
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
|
|
||||||
BooleanSetting.AUDIO_MUTED.setBoolean(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEmulationStarted() {
|
|
||||||
emulationViewModel.setEmulationStarted(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEmulationStopped(status: Int) {
|
|
||||||
if (status == 0 && emulationViewModel.programChanged.value == -1) {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
emulationViewModel.setEmulationStopped(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onProgramChanged(programIndex: Int) {
|
|
||||||
emulationViewModel.setProgramChanged(programIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startMotionSensorListener() {
|
|
||||||
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
||||||
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
|
||||||
val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
||||||
sensorManager.registerListener(this, gyroSensor, SensorManager.SENSOR_DELAY_GAME)
|
|
||||||
sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_GAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopMotionSensorListener() {
|
|
||||||
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
||||||
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
|
||||||
val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
||||||
|
|
||||||
sensorManager.unregisterListener(this, gyroSensor)
|
|
||||||
sensorManager.unregisterListener(this, accelSensor)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val EXTRA_SELECTED_GAME = "SelectedGame"
|
|
||||||
|
|
||||||
fun launch(activity: AppCompatActivity, game: Game) {
|
|
||||||
val launcher = Intent(activity, EmulationActivity::class.java)
|
|
||||||
launcher.putExtra(EXTRA_SELECTED_GAME, game)
|
|
||||||
activity.startActivity(launcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun areCoordinatesOutside(view: View?, x: Float, y: Float): Boolean {
|
|
||||||
if (view == null) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val viewBounds = Rect()
|
|
||||||
view.getGlobalVisibleRect(viewBounds)
|
|
||||||
return !viewBounds.contains(x.roundToInt(), y.roundToInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.adapters
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate
|
|
||||||
* code used in every [RecyclerView].
|
|
||||||
* Type assigned to [Model] must inherit from [Object] in order to be compared properly.
|
|
||||||
* @param exact Decides whether each item will be compared by reference or by their contents
|
|
||||||
*/
|
|
||||||
abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>>(
|
|
||||||
exact: Boolean = true
|
|
||||||
) : ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>(exact)).build()) {
|
|
||||||
override fun onBindViewHolder(holder: Holder, position: Int) =
|
|
||||||
holder.bind(currentList[position])
|
|
||||||
|
|
||||||
private class DiffCallback<Model>(val exact: Boolean) : DiffUtil.ItemCallback<Model>() {
|
|
||||||
override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
|
|
||||||
if (exact) {
|
|
||||||
return oldItem === newItem
|
|
||||||
}
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("DiffUtilEquals")
|
|
||||||
override fun areContentsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.adapters
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ListItemAddonBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.Patch
|
|
||||||
import org.sudachi.sudachi_emu.model.AddonViewModel
|
|
||||||
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
|
|
||||||
|
|
||||||
class AddonAdapter(val addonViewModel: AddonViewModel) :
|
|
||||||
AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
|
|
||||||
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
.also { return AddonViewHolder(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
|
|
||||||
AbstractViewHolder<Patch>(binding) {
|
|
||||||
override fun bind(model: Patch) {
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked
|
|
||||||
}
|
|
||||||
binding.title.text = model.name
|
|
||||||
binding.version.text = model.version
|
|
||||||
binding.addonCheckbox.setOnCheckedChangeListener { _, checked ->
|
|
||||||
model.enabled = checked
|
|
||||||
}
|
|
||||||
binding.addonCheckbox.isChecked = model.enabled
|
|
||||||
binding.buttonDelete.setOnClickListener {
|
|
||||||
addonViewModel.setAddonToDelete(model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.adapters
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import org.sudachi.sudachi_emu.HomeNavigationDirections
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.databinding.CardSimpleOutlinedBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.Applet
|
|
||||||
import org.sudachi.sudachi_emu.model.AppletInfo
|
|
||||||
import org.sudachi.sudachi_emu.model.Game
|
|
||||||
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
|
|
||||||
|
|
||||||
class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) :
|
|
||||||
AbstractListAdapter<Applet, AppletAdapter.AppletViewHolder>(applets) {
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): AppletAdapter.AppletViewHolder {
|
|
||||||
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
.also { return AppletViewHolder(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
|
|
||||||
AbstractViewHolder<Applet>(binding) {
|
|
||||||
override fun bind(model: Applet) {
|
|
||||||
binding.title.setText(model.titleId)
|
|
||||||
binding.description.setText(model.descriptionId)
|
|
||||||
binding.icon.setImageDrawable(
|
|
||||||
ResourcesCompat.getDrawable(
|
|
||||||
binding.icon.context.resources,
|
|
||||||
model.iconId,
|
|
||||||
binding.icon.context.theme
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.root.setOnClickListener { onClick(model) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onClick(applet: Applet) {
|
|
||||||
val appletPath = NativeLibrary.getAppletLaunchPath(applet.appletInfo.entryId)
|
|
||||||
if (appletPath.isEmpty()) {
|
|
||||||
Toast.makeText(
|
|
||||||
binding.root.context,
|
|
||||||
R.string.applets_error_applet,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (applet.appletInfo == AppletInfo.Cabinet) {
|
|
||||||
binding.root.findNavController()
|
|
||||||
.navigate(R.id.action_appletLauncherFragment_to_cabinetLauncherDialogFragment)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.setCurrentAppletId(applet.appletInfo.appletId)
|
|
||||||
val appletGame = Game(
|
|
||||||
title = SudachiApplication.appContext.getString(applet.titleId),
|
|
||||||
path = appletPath
|
|
||||||
)
|
|
||||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
|
|
||||||
binding.root.findNavController().navigate(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.adapters
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import org.sudachi.sudachi_emu.databinding.CardFolderBinding
|
|
||||||
import org.sudachi.sudachi_emu.fragments.GameFolderPropertiesDialogFragment
|
|
||||||
import org.sudachi.sudachi_emu.model.GameDir
|
|
||||||
import org.sudachi.sudachi_emu.model.GamesViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.marquee
|
|
||||||
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
|
|
||||||
|
|
||||||
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
|
|
||||||
AbstractDiffAdapter<GameDir, FolderAdapter.FolderViewHolder>() {
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): FolderAdapter.FolderViewHolder {
|
|
||||||
CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
.also { return FolderViewHolder(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class FolderViewHolder(val binding: CardFolderBinding) :
|
|
||||||
AbstractViewHolder<GameDir>(binding) {
|
|
||||||
override fun bind(model: GameDir) {
|
|
||||||
binding.apply {
|
|
||||||
path.text = Uri.parse(model.uriString).path
|
|
||||||
path.marquee()
|
|
||||||
|
|
||||||
buttonEdit.setOnClickListener {
|
|
||||||
GameFolderPropertiesDialogFragment.newInstance(model)
|
|
||||||
.show(
|
|
||||||
activity.supportFragmentManager,
|
|
||||||
GameFolderPropertiesDialogFragment.TAG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonDelete.setOnClickListener {
|
|
||||||
gamesViewModel.removeFolder(model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.adapters
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.sudachi.sudachi_emu.HomeNavigationDirections
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.databinding.CardGameBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.Game
|
|
||||||
import org.sudachi.sudachi_emu.model.GamesViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.GameIconUtils
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.marquee
|
|
||||||
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
|
|
||||||
|
|
||||||
class GameAdapter(private val activity: AppCompatActivity) :
|
|
||||||
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
|
||||||
CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
.also { return GameViewHolder(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
|
||||||
AbstractViewHolder<Game>(binding) {
|
|
||||||
override fun bind(model: Game) {
|
|
||||||
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
||||||
GameIconUtils.loadGameIcon(model, binding.imageGameScreen)
|
|
||||||
|
|
||||||
binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
|
|
||||||
|
|
||||||
binding.textGameTitle.marquee()
|
|
||||||
binding.cardGame.setOnClickListener { onClick(model) }
|
|
||||||
binding.cardGame.setOnLongClickListener { onLongClick(model) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onClick(game: Game) {
|
|
||||||
val gameExists = DocumentFile.fromSingleUri(
|
|
||||||
SudachiApplication.appContext,
|
|
||||||
Uri.parse(game.path)
|
|
||||||
)?.exists() == true
|
|
||||||
if (!gameExists) {
|
|
||||||
Toast.makeText(
|
|
||||||
SudachiApplication.appContext,
|
|
||||||
R.string.loader_error_file_not_found,
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val preferences =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(SudachiApplication.appContext)
|
|
||||||
preferences.edit()
|
|
||||||
.putLong(
|
|
||||||
game.keyLastPlayedTime,
|
|
||||||
System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
activity.lifecycleScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val shortcut =
|
|
||||||
ShortcutInfoCompat.Builder(SudachiApplication.appContext, game.path)
|
|
||||||
.setShortLabel(game.title)
|
|
||||||
.setIcon(GameIconUtils.getShortcutIcon(activity, game))
|
|
||||||
.setIntent(game.launchIntent)
|
|
||||||
.build()
|
|
||||||
ShortcutManagerCompat.pushDynamicShortcut(SudachiApplication.appContext, shortcut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(game, true)
|
|
||||||
binding.root.findNavController().navigate(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onLongClick(game: Game): Boolean {
|
|
||||||
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
|
|
||||||
binding.root.findNavController().navigate(action)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.adapters
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
|
|
||||||
import org.sudachi.sudachi_emu.fragments.LicenseBottomSheetDialogFragment
|
|
||||||
import org.sudachi.sudachi_emu.model.License
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
|
|
||||||
|
|
||||||
class LicenseAdapter(private val activity: AppCompatActivity, licenses: List<License>) :
|
|
||||||
AbstractListAdapter<License, LicenseAdapter.LicenseViewHolder>(licenses) {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
|
|
||||||
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
.also { return LicenseViewHolder(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class LicenseViewHolder(val binding: ListItemSettingBinding) :
|
|
||||||
AbstractViewHolder<License>(binding) {
|
|
||||||
override fun bind(model: License) {
|
|
||||||
binding.apply {
|
|
||||||
textSettingName.text = root.context.getString(model.titleId)
|
|
||||||
textSettingDescription.text = root.context.getString(model.descriptionId)
|
|
||||||
textSettingValue.setVisible(false)
|
|
||||||
|
|
||||||
root.setOnClickListener { onClick(model) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onClick(license: License) {
|
|
||||||
LicenseBottomSheetDialogFragment.newInstance(license)
|
|
||||||
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.adapters
|
|
||||||
|
|
||||||
import android.text.Html
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
import org.sudachi.sudachi_emu.databinding.PageSetupBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.SetupCallback
|
|
||||||
import org.sudachi.sudachi_emu.model.SetupPage
|
|
||||||
import org.sudachi.sudachi_emu.model.StepState
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
import org.sudachi.sudachi_emu.viewholder.AbstractViewHolder
|
|
||||||
|
|
||||||
class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
|
|
||||||
AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
|
|
||||||
PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
.also { return SetupPageViewHolder(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
|
|
||||||
AbstractViewHolder<SetupPage>(binding), SetupCallback {
|
|
||||||
override fun bind(model: SetupPage) {
|
|
||||||
if (model.stepCompleted.invoke() == StepState.COMPLETE) {
|
|
||||||
binding.buttonAction.setVisible(visible = false, gone = false)
|
|
||||||
binding.textConfirmation.setVisible(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.icon.setImageDrawable(
|
|
||||||
ResourcesCompat.getDrawable(
|
|
||||||
activity.resources,
|
|
||||||
model.iconId,
|
|
||||||
activity.theme
|
|
||||||
)
|
|
||||||
)
|
|
||||||
binding.textTitle.text = activity.resources.getString(model.titleId)
|
|
||||||
binding.textDescription.text =
|
|
||||||
Html.fromHtml(activity.resources.getString(model.descriptionId), 0)
|
|
||||||
|
|
||||||
binding.buttonAction.apply {
|
|
||||||
text = activity.resources.getString(model.buttonTextId)
|
|
||||||
if (model.buttonIconId != 0) {
|
|
||||||
icon = ResourcesCompat.getDrawable(
|
|
||||||
activity.resources,
|
|
||||||
model.buttonIconId,
|
|
||||||
activity.theme
|
|
||||||
)
|
|
||||||
}
|
|
||||||
iconGravity =
|
|
||||||
if (model.leftAlignedIcon) {
|
|
||||||
MaterialButton.ICON_GRAVITY_START
|
|
||||||
} else {
|
|
||||||
MaterialButton.ICON_GRAVITY_END
|
|
||||||
}
|
|
||||||
setOnClickListener {
|
|
||||||
model.buttonAction.invoke(this@SetupPageViewHolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStepCompleted() {
|
|
||||||
ViewUtils.hideView(binding.buttonAction, 200)
|
|
||||||
ViewUtils.showView(binding.textConfirmation, 200)
|
|
||||||
ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.disk_shader_cache
|
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.activities.EmulationActivity
|
|
||||||
import org.sudachi.sudachi_emu.model.EmulationViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.Log
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
object DiskShaderCacheProgress {
|
|
||||||
private lateinit var emulationViewModel: EmulationViewModel
|
|
||||||
|
|
||||||
private fun prepareViewModel() {
|
|
||||||
emulationViewModel =
|
|
||||||
ViewModelProvider(
|
|
||||||
NativeLibrary.sEmulationActivity.get() as EmulationActivity
|
|
||||||
)[EmulationViewModel::class.java]
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun loadProgress(stage: Int, progress: Int, max: Int) {
|
|
||||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
|
||||||
if (emulationActivity == null) {
|
|
||||||
Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emulationActivity.runOnUiThread {
|
|
||||||
when (LoadCallbackStage.values()[stage]) {
|
|
||||||
LoadCallbackStage.Prepare -> prepareViewModel()
|
|
||||||
LoadCallbackStage.Build -> emulationViewModel.updateProgress(
|
|
||||||
emulationActivity.getString(R.string.building_shaders),
|
|
||||||
progress,
|
|
||||||
max
|
|
||||||
)
|
|
||||||
|
|
||||||
LoadCallbackStage.Complete -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equivalent to VideoCore::LoadCallbackStage
|
|
||||||
enum class LoadCallbackStage {
|
|
||||||
Prepare, Build, Complete
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,416 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.input
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeButton
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.InputType
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.ButtonName
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NpadStyleIndex
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
import org.sudachi.sudachi_emu.utils.ParamPackage
|
|
||||||
import android.view.InputDevice
|
|
||||||
|
|
||||||
object NativeInput {
|
|
||||||
/**
|
|
||||||
* Default controller id for each device
|
|
||||||
*/
|
|
||||||
const val Player1Device = 0
|
|
||||||
const val Player2Device = 1
|
|
||||||
const val Player3Device = 2
|
|
||||||
const val Player4Device = 3
|
|
||||||
const val Player5Device = 4
|
|
||||||
const val Player6Device = 5
|
|
||||||
const val Player7Device = 6
|
|
||||||
const val Player8Device = 7
|
|
||||||
const val ConsoleDevice = 8
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Button states
|
|
||||||
*/
|
|
||||||
object ButtonState {
|
|
||||||
const val RELEASED = 0
|
|
||||||
const val PRESSED = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if pro controller isn't available and handheld is.
|
|
||||||
* Intended to check where the input overlay should direct its inputs.
|
|
||||||
*/
|
|
||||||
external fun isHandheldOnly(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles button press events for a gamepad.
|
|
||||||
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
|
||||||
* @param port Port determined by controller connection order.
|
|
||||||
* @param buttonId The Android Keycode corresponding to this event.
|
|
||||||
* @param action Mask identifying which action is happening (button pressed down, or button released).
|
|
||||||
*/
|
|
||||||
external fun onGamePadButtonEvent(
|
|
||||||
guid: String,
|
|
||||||
port: Int,
|
|
||||||
buttonId: Int,
|
|
||||||
action: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles axis movement events.
|
|
||||||
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
|
||||||
* @param port Port determined by controller connection order.
|
|
||||||
* @param axis The axis ID.
|
|
||||||
* @param value Value along the given axis.
|
|
||||||
*/
|
|
||||||
external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles motion events.
|
|
||||||
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
|
||||||
* @param port Port determined by controller connection order.
|
|
||||||
* @param deltaTimestamp The finger id corresponding to this event.
|
|
||||||
* @param xGyro The value of the x-axis for the gyroscope.
|
|
||||||
* @param yGyro The value of the y-axis for the gyroscope.
|
|
||||||
* @param zGyro The value of the z-axis for the gyroscope.
|
|
||||||
* @param xAccel The value of the x-axis for the accelerometer.
|
|
||||||
* @param yAccel The value of the y-axis for the accelerometer.
|
|
||||||
* @param zAccel The value of the z-axis for the accelerometer.
|
|
||||||
*/
|
|
||||||
external fun onGamePadMotionEvent(
|
|
||||||
guid: String,
|
|
||||||
port: Int,
|
|
||||||
deltaTimestamp: Long,
|
|
||||||
xGyro: Float,
|
|
||||||
yGyro: Float,
|
|
||||||
zGyro: Float,
|
|
||||||
xAccel: Float,
|
|
||||||
yAccel: Float,
|
|
||||||
zAccel: Float
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signals and load a nfc tag
|
|
||||||
* @param data Byte array containing all the data from a nfc tag.
|
|
||||||
*/
|
|
||||||
external fun onReadNfcTag(data: ByteArray?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes current loaded nfc tag.
|
|
||||||
*/
|
|
||||||
external fun onRemoveNfcTag()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles touch press events.
|
|
||||||
* @param fingerId The finger id corresponding to this event.
|
|
||||||
* @param xAxis The value of the x-axis on the touchscreen.
|
|
||||||
* @param yAxis The value of the y-axis on the touchscreen.
|
|
||||||
*/
|
|
||||||
external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles touch movement.
|
|
||||||
* @param fingerId The finger id corresponding to this event.
|
|
||||||
* @param xAxis The value of the x-axis on the touchscreen.
|
|
||||||
* @param yAxis The value of the y-axis on the touchscreen.
|
|
||||||
*/
|
|
||||||
external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles touch release events.
|
|
||||||
* @param fingerId The finger id corresponding to this event
|
|
||||||
*/
|
|
||||||
external fun onTouchReleased(fingerId: Int)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a button input to the global virtual controllers.
|
|
||||||
* @param port Port determined by controller connection order.
|
|
||||||
* @param button The [NativeButton] corresponding to this event.
|
|
||||||
* @param action Mask identifying which action is happening (button pressed down, or button released).
|
|
||||||
*/
|
|
||||||
fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) =
|
|
||||||
onOverlayButtonEventImpl(port, button.int, action)
|
|
||||||
|
|
||||||
private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a joystick input to the global virtual controllers.
|
|
||||||
* @param port Port determined by controller connection order.
|
|
||||||
* @param stick The [NativeAnalog] corresponding to this event.
|
|
||||||
* @param xAxis Value along the X axis.
|
|
||||||
* @param yAxis Value along the Y axis.
|
|
||||||
*/
|
|
||||||
fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) =
|
|
||||||
onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis)
|
|
||||||
|
|
||||||
private external fun onOverlayJoystickEventImpl(
|
|
||||||
port: Int,
|
|
||||||
stickId: Int,
|
|
||||||
xAxis: Float,
|
|
||||||
yAxis: Float
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles motion events for the global virtual controllers.
|
|
||||||
* @param port Port determined by controller connection order
|
|
||||||
* @param deltaTimestamp The finger id corresponding to this event.
|
|
||||||
* @param xGyro The value of the x-axis for the gyroscope.
|
|
||||||
* @param yGyro The value of the y-axis for the gyroscope.
|
|
||||||
* @param zGyro The value of the z-axis for the gyroscope.
|
|
||||||
* @param xAccel The value of the x-axis for the accelerometer.
|
|
||||||
* @param yAccel The value of the y-axis for the accelerometer.
|
|
||||||
* @param zAccel The value of the z-axis for the accelerometer.
|
|
||||||
*/
|
|
||||||
external fun onDeviceMotionEvent(
|
|
||||||
port: Int,
|
|
||||||
deltaTimestamp: Long,
|
|
||||||
xGyro: Float,
|
|
||||||
yGyro: Float,
|
|
||||||
zGyro: Float,
|
|
||||||
xAccel: Float,
|
|
||||||
yAccel: Float,
|
|
||||||
zAccel: Float
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reloads all input devices from the currently loaded Settings::values.players into HID Core
|
|
||||||
*/
|
|
||||||
external fun reloadInputDevices()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a controller to be used with mapping
|
|
||||||
* @param device An [InputDevice] or the input overlay wrapped with [SudachiInputDevice]
|
|
||||||
*/
|
|
||||||
external fun registerController(device: SudachiInputDevice)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the names of input devices that have been registered with the input subsystem via [registerController]
|
|
||||||
*/
|
|
||||||
external fun getInputDevices(): Array<String>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads all input profiles from disk. Must be called before creating a profile picker.
|
|
||||||
*/
|
|
||||||
external fun loadInputProfiles()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the names of each available input profile.
|
|
||||||
*/
|
|
||||||
external fun getInputProfileNames(): Array<String>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the user-provided name for an input profile is valid.
|
|
||||||
* @param name User-provided name for an input profile.
|
|
||||||
* @return Whether [name] is valid or not.
|
|
||||||
*/
|
|
||||||
external fun isProfileNameValid(name: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new input profile.
|
|
||||||
* @param name The new profile's name.
|
|
||||||
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
|
|
||||||
* name to this player's config.
|
|
||||||
* @return Whether creating the profile was successful or not.
|
|
||||||
*/
|
|
||||||
external fun createProfile(name: String, playerIndex: Int): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an input profile.
|
|
||||||
* @param name Name of the profile to delete.
|
|
||||||
* @param playerIndex Index of the player that's currently being edited. Used to remove the profile
|
|
||||||
* name from this player's config if they have it loaded.
|
|
||||||
* @return Whether deleting this profile was successful or not.
|
|
||||||
*/
|
|
||||||
external fun deleteProfile(name: String, playerIndex: Int): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an input profile.
|
|
||||||
* @param name Name of the input profile to load.
|
|
||||||
* @param playerIndex Index of the player that will have this profile loaded.
|
|
||||||
* @return Whether loading this profile was successful or not.
|
|
||||||
*/
|
|
||||||
external fun loadProfile(name: String, playerIndex: Int): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves an input profile.
|
|
||||||
* @param name Name of the profile to save.
|
|
||||||
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
|
|
||||||
* name to this player's config.
|
|
||||||
* @return Whether saving the profile was successful or not.
|
|
||||||
*/
|
|
||||||
external fun saveProfile(name: String, playerIndex: Int): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues]
|
|
||||||
* Must be used while per-game config is loaded.
|
|
||||||
*/
|
|
||||||
external fun loadPerGameConfiguration(
|
|
||||||
playerIndex: Int,
|
|
||||||
selectedIndex: Int,
|
|
||||||
selectedProfileName: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the input subsystem to start listening for inputs to map.
|
|
||||||
* @param type Type of input to map as shown by the int property in each [InputType].
|
|
||||||
*/
|
|
||||||
external fun beginMapping(type: Int)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping.
|
|
||||||
* Must be run after [beginMapping] and before [stopMapping].
|
|
||||||
*/
|
|
||||||
external fun getNextInput(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the input subsystem to stop listening for inputs to map.
|
|
||||||
*/
|
|
||||||
external fun stopMapping()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a controller's mappings with auto-mapping params.
|
|
||||||
* @param playerIndex Index of the player to auto-map.
|
|
||||||
* @param deviceParams [ParamPackage] representing the device to auto-map as received
|
|
||||||
* from [getInputDevices].
|
|
||||||
* @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams].
|
|
||||||
* Intended to be a way to provide a default name for a controller if the "display" param is empty.
|
|
||||||
*/
|
|
||||||
fun updateMappingsWithDefault(
|
|
||||||
playerIndex: Int,
|
|
||||||
deviceParams: ParamPackage,
|
|
||||||
displayName: String
|
|
||||||
) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName)
|
|
||||||
|
|
||||||
private external fun updateMappingsWithDefaultImpl(
|
|
||||||
playerIndex: Int,
|
|
||||||
deviceParams: String,
|
|
||||||
displayName: String
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the params for a specific button.
|
|
||||||
* @param playerIndex Index of the player to get params from.
|
|
||||||
* @param button The [NativeButton] to get params for.
|
|
||||||
* @return A [ParamPackage] representing a player's specific button.
|
|
||||||
*/
|
|
||||||
fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage =
|
|
||||||
ParamPackage(getButtonParamImpl(playerIndex, button.int))
|
|
||||||
|
|
||||||
private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the params for a specific button.
|
|
||||||
* @param playerIndex Index of the player to set params for.
|
|
||||||
* @param button The [NativeButton] to set params for.
|
|
||||||
* @param param A [ParamPackage] to set.
|
|
||||||
*/
|
|
||||||
fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) =
|
|
||||||
setButtonParamImpl(playerIndex, button.int, param.serialize())
|
|
||||||
|
|
||||||
private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the params for a specific stick.
|
|
||||||
* @param playerIndex Index of the player to get params from.
|
|
||||||
* @param stick The [NativeAnalog] to get params for.
|
|
||||||
* @return A [ParamPackage] representing a player's specific stick.
|
|
||||||
*/
|
|
||||||
fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage =
|
|
||||||
ParamPackage(getStickParamImpl(playerIndex, stick.int))
|
|
||||||
|
|
||||||
private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the params for a specific stick.
|
|
||||||
* @param playerIndex Index of the player to set params for.
|
|
||||||
* @param stick The [NativeAnalog] to set params for.
|
|
||||||
* @param param A [ParamPackage] to set.
|
|
||||||
*/
|
|
||||||
fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) =
|
|
||||||
setStickParamImpl(playerIndex, stick.int, param.serialize())
|
|
||||||
|
|
||||||
private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for
|
|
||||||
* a button/analog/other.
|
|
||||||
* @param param A [ParamPackage] that represents a specific button's params.
|
|
||||||
* @return The [ButtonName] for [param].
|
|
||||||
*/
|
|
||||||
fun getButtonName(param: ParamPackage): ButtonName =
|
|
||||||
ButtonName.from(getButtonNameImpl(param.serialize()))
|
|
||||||
|
|
||||||
private external fun getButtonNameImpl(param: String): Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets each supported [NpadStyleIndex] for a given player.
|
|
||||||
* @param playerIndex Index of the player to get supported indexes for.
|
|
||||||
* @return List of each supported [NpadStyleIndex].
|
|
||||||
*/
|
|
||||||
fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> =
|
|
||||||
getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) }
|
|
||||||
|
|
||||||
private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the [NpadStyleIndex] for a given player.
|
|
||||||
* @param playerIndex Index of the player to get an [NpadStyleIndex] from.
|
|
||||||
* @return The [NpadStyleIndex] for a given player.
|
|
||||||
*/
|
|
||||||
fun getStyleIndex(playerIndex: Int): NpadStyleIndex =
|
|
||||||
NpadStyleIndex.from(getStyleIndexImpl(playerIndex))
|
|
||||||
|
|
||||||
private external fun getStyleIndexImpl(playerIndex: Int): Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the [NpadStyleIndex] for a given player.
|
|
||||||
* @param playerIndex Index of the player to change.
|
|
||||||
* @param style The new style to set.
|
|
||||||
*/
|
|
||||||
fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) =
|
|
||||||
setStyleIndexImpl(playerIndex, style.int)
|
|
||||||
|
|
||||||
private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a device is a controller.
|
|
||||||
* @param params [ParamPackage] for an input device retrieved from [getInputDevices]
|
|
||||||
* @return Whether the device is a controller or not.
|
|
||||||
*/
|
|
||||||
fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize())
|
|
||||||
|
|
||||||
private external fun isControllerImpl(params: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a controller is connected
|
|
||||||
* @param playerIndex Index of the player to check.
|
|
||||||
* @return Whether the player is connected or not.
|
|
||||||
*/
|
|
||||||
external fun getIsConnected(playerIndex: Int): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects/disconnects a controller and ensures that connection order stays in-tact.
|
|
||||||
* @param playerIndex Index of the player to connect/disconnect.
|
|
||||||
* @param connected Whether to connect or disconnect this controller.
|
|
||||||
*/
|
|
||||||
fun connectControllers(playerIndex: Int, connected: Boolean = true) {
|
|
||||||
val connectedControllers = mutableListOf<Boolean>().apply {
|
|
||||||
if (connected) {
|
|
||||||
for (i in 0 until 8) {
|
|
||||||
add(i <= playerIndex)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (i in 0 until 8) {
|
|
||||||
add(i < playerIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connectControllersImpl(connectedControllers.toBooleanArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun connectControllersImpl(connected: BooleanArray)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets all of the button and analog mappings for a player.
|
|
||||||
* @param playerIndex Index of the player that will have its mappings reset.
|
|
||||||
*/
|
|
||||||
external fun resetControllerMappings(playerIndex: Int)
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.input
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.CombinedVibration
|
|
||||||
import android.os.VibrationEffect
|
|
||||||
import android.os.Vibrator
|
|
||||||
import android.os.VibratorManager
|
|
||||||
import android.view.InputDevice
|
|
||||||
import androidx.annotation.Keep
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
interface SudachiVibrator {
|
|
||||||
fun supportsVibration(): Boolean
|
|
||||||
|
|
||||||
fun vibrate(intensity: Float)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun getControllerVibrator(device: InputDevice): SudachiVibrator =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
SudachiVibratorManager(device.vibratorManager)
|
|
||||||
} else {
|
|
||||||
SudachiVibratorManagerCompat(device.vibrator)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSystemVibrator(): SudachiVibrator =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
val vibratorManager = SudachiApplication.appContext
|
|
||||||
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
|
||||||
SudachiVibratorManager(vibratorManager)
|
|
||||||
} else {
|
|
||||||
val vibrator = SudachiApplication.appContext
|
|
||||||
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
||||||
SudachiVibratorManagerCompat(vibrator)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getVibrationEffect(intensity: Float): VibrationEffect? {
|
|
||||||
if (intensity > 0f) {
|
|
||||||
return VibrationEffect.createOneShot(
|
|
||||||
50,
|
|
||||||
(255.0 * intensity).toInt().coerceIn(1, 255)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.S)
|
|
||||||
class SudachiVibratorManager(private val vibratorManager: VibratorManager) : SudachiVibrator {
|
|
||||||
override fun supportsVibration(): Boolean {
|
|
||||||
return vibratorManager.vibratorIds.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun vibrate(intensity: Float) {
|
|
||||||
val vibration = SudachiVibrator.getVibrationEffect(intensity) ?: return
|
|
||||||
vibratorManager.vibrate(CombinedVibration.createParallel(vibration))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SudachiVibratorManagerCompat(private val vibrator: Vibrator) : SudachiVibrator {
|
|
||||||
override fun supportsVibration(): Boolean {
|
|
||||||
return vibrator.hasVibrator()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun vibrate(intensity: Float) {
|
|
||||||
val vibration = SudachiVibrator.getVibrationEffect(intensity) ?: return
|
|
||||||
vibrator.vibrate(vibration)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.input.model
|
|
||||||
|
|
||||||
enum class AnalogDirection(val int: Int, val param: String) {
|
|
||||||
Up(0, "up"),
|
|
||||||
Down(1, "down"),
|
|
||||||
Left(2, "left"),
|
|
||||||
Right(3, "right")
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.input.model
|
|
||||||
|
|
||||||
// Loosely matches the enum in common/input.h
|
|
||||||
enum class ButtonName(val int: Int) {
|
|
||||||
Invalid(1),
|
|
||||||
|
|
||||||
// This will display the engine name instead of the button name
|
|
||||||
Engine(2),
|
|
||||||
|
|
||||||
// This will display the button by value instead of the button name
|
|
||||||
Value(3);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.input.model
|
|
||||||
|
|
||||||
// Must match enum in src/common/settings_input.h
|
|
||||||
enum class NativeAnalog(val int: Int) {
|
|
||||||
LStick(0),
|
|
||||||
RStick(1);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.input.model
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
|
|
||||||
// Must match enum in src/core/hid/hid_types.h
|
|
||||||
enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) {
|
|
||||||
None(0),
|
|
||||||
Fullkey(3, R.string.pro_controller),
|
|
||||||
Handheld(4, R.string.handheld),
|
|
||||||
HandheldNES(4),
|
|
||||||
JoyconDual(5, R.string.dual_joycons),
|
|
||||||
JoyconLeft(6, R.string.left_joycon),
|
|
||||||
JoyconRight(7, R.string.right_joycon),
|
|
||||||
GameCube(8, R.string.gamecube_controller),
|
|
||||||
Pokeball(9),
|
|
||||||
NES(10),
|
|
||||||
SNES(12),
|
|
||||||
N64(13),
|
|
||||||
SegaGenesis(14),
|
|
||||||
SystemExt(32),
|
|
||||||
System(33);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.input.model
|
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
data class PlayerInput(
|
|
||||||
var connected: Boolean,
|
|
||||||
var buttons: Array<String>,
|
|
||||||
var analogs: Array<String>,
|
|
||||||
var motions: Array<String>,
|
|
||||||
|
|
||||||
var vibrationEnabled: Boolean,
|
|
||||||
var vibrationStrength: Int,
|
|
||||||
|
|
||||||
var bodyColorLeft: Long,
|
|
||||||
var bodyColorRight: Long,
|
|
||||||
var buttonColorLeft: Long,
|
|
||||||
var buttonColorRight: Long,
|
|
||||||
var profileName: String,
|
|
||||||
|
|
||||||
var useSystemVibrator: Boolean
|
|
||||||
) {
|
|
||||||
// It's recommended to use the generated equals() and hashCode() methods
|
|
||||||
// when using arrays in a data class
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as PlayerInput
|
|
||||||
|
|
||||||
if (connected != other.connected) return false
|
|
||||||
if (!buttons.contentEquals(other.buttons)) return false
|
|
||||||
if (!analogs.contentEquals(other.analogs)) return false
|
|
||||||
if (!motions.contentEquals(other.motions)) return false
|
|
||||||
if (vibrationEnabled != other.vibrationEnabled) return false
|
|
||||||
if (vibrationStrength != other.vibrationStrength) return false
|
|
||||||
if (bodyColorLeft != other.bodyColorLeft) return false
|
|
||||||
if (bodyColorRight != other.bodyColorRight) return false
|
|
||||||
if (buttonColorLeft != other.buttonColorLeft) return false
|
|
||||||
if (buttonColorRight != other.buttonColorRight) return false
|
|
||||||
if (profileName != other.profileName) return false
|
|
||||||
return useSystemVibrator == other.useSystemVibrator
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = connected.hashCode()
|
|
||||||
result = 31 * result + buttons.contentHashCode()
|
|
||||||
result = 31 * result + analogs.contentHashCode()
|
|
||||||
result = 31 * result + motions.contentHashCode()
|
|
||||||
result = 31 * result + vibrationEnabled.hashCode()
|
|
||||||
result = 31 * result + vibrationStrength
|
|
||||||
result = 31 * result + bodyColorLeft.hashCode()
|
|
||||||
result = 31 * result + bodyColorRight.hashCode()
|
|
||||||
result = 31 * result + buttonColorLeft.hashCode()
|
|
||||||
result = 31 * result + buttonColorRight.hashCode()
|
|
||||||
result = 31 * result + profileName.hashCode()
|
|
||||||
result = 31 * result + useSystemVibrator.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasMapping(): Boolean {
|
|
||||||
var hasMapping = false
|
|
||||||
buttons.forEach {
|
|
||||||
if (it != "[empty]" && it.isNotEmpty()) {
|
|
||||||
hasMapping = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
analogs.forEach {
|
|
||||||
if (it != "[empty]" && it.isNotEmpty()) {
|
|
||||||
hasMapping = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
motions.forEach {
|
|
||||||
if (it != "[empty]" && it.isNotEmpty()) {
|
|
||||||
hasMapping = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasMapping
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
interface AbstractFloatSetting : AbstractSetting {
|
|
||||||
fun getFloat(needsGlobal: Boolean = false): Float
|
|
||||||
fun setFloat(value: Float)
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
interface AbstractIntSetting : AbstractSetting {
|
|
||||||
fun getInt(needsGlobal: Boolean = false): Int
|
|
||||||
fun setInt(value: Int)
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
interface AbstractStringSetting : AbstractSetting {
|
|
||||||
fun getString(needsGlobal: Boolean = false): String
|
|
||||||
fun setString(value: String)
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
enum class ByteSetting(override val key: String) : AbstractByteSetting {
|
|
||||||
AUDIO_VOLUME("volume");
|
|
||||||
|
|
||||||
override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
|
|
||||||
|
|
||||||
override fun setByte(value: Byte) {
|
|
||||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
|
||||||
global = false
|
|
||||||
}
|
|
||||||
NativeConfig.setByte(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() }
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setByte(key, defaultValue)
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
enum class FloatSetting(override val key: String) : AbstractFloatSetting {
|
|
||||||
// No float settings currently exist
|
|
||||||
EMPTY_SETTING("");
|
|
||||||
|
|
||||||
override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
|
|
||||||
|
|
||||||
override fun setFloat(value: Float) {
|
|
||||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
|
||||||
global = false
|
|
||||||
}
|
|
||||||
NativeConfig.setFloat(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() }
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setFloat(key, defaultValue)
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
enum class LongSetting(override val key: String) : AbstractLongSetting {
|
|
||||||
CUSTOM_RTC("custom_rtc");
|
|
||||||
|
|
||||||
override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
|
|
||||||
|
|
||||||
override fun setLong(value: Long) {
|
|
||||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
|
||||||
global = false
|
|
||||||
}
|
|
||||||
NativeConfig.setLong(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() }
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setLong(key, defaultValue)
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
|
|
||||||
object Settings {
|
|
||||||
enum class MenuTag(val titleId: Int = 0) {
|
|
||||||
SECTION_ROOT(R.string.advanced_settings),
|
|
||||||
SECTION_SYSTEM(R.string.preferences_system),
|
|
||||||
SECTION_RENDERER(R.string.preferences_graphics),
|
|
||||||
SECTION_AUDIO(R.string.preferences_audio),
|
|
||||||
SECTION_INPUT(R.string.preferences_controls),
|
|
||||||
SECTION_INPUT_PLAYER_ONE,
|
|
||||||
SECTION_INPUT_PLAYER_TWO,
|
|
||||||
SECTION_INPUT_PLAYER_THREE,
|
|
||||||
SECTION_INPUT_PLAYER_FOUR,
|
|
||||||
SECTION_INPUT_PLAYER_FIVE,
|
|
||||||
SECTION_INPUT_PLAYER_SIX,
|
|
||||||
SECTION_INPUT_PLAYER_SEVEN,
|
|
||||||
SECTION_INPUT_PLAYER_EIGHT,
|
|
||||||
SECTION_THEME(R.string.preferences_theme),
|
|
||||||
SECTION_DEBUG(R.string.preferences_debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPlayerString(player: Int): String =
|
|
||||||
SudachiApplication.appContext.getString(R.string.preferences_player, player)
|
|
||||||
|
|
||||||
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
|
|
||||||
const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
|
|
||||||
|
|
||||||
// Deprecated input overlay preference keys
|
|
||||||
const val PREF_CONTROL_SCALE = "controlScale"
|
|
||||||
const val PREF_CONTROL_OPACITY = "controlOpacity"
|
|
||||||
const val PREF_TOUCH_ENABLED = "isTouchEnabled"
|
|
||||||
const val PREF_BUTTON_A = "buttonToggle0"
|
|
||||||
const val PREF_BUTTON_B = "buttonToggle1"
|
|
||||||
const val PREF_BUTTON_X = "buttonToggle2"
|
|
||||||
const val PREF_BUTTON_Y = "buttonToggle3"
|
|
||||||
const val PREF_BUTTON_L = "buttonToggle4"
|
|
||||||
const val PREF_BUTTON_R = "buttonToggle5"
|
|
||||||
const val PREF_BUTTON_ZL = "buttonToggle6"
|
|
||||||
const val PREF_BUTTON_ZR = "buttonToggle7"
|
|
||||||
const val PREF_BUTTON_PLUS = "buttonToggle8"
|
|
||||||
const val PREF_BUTTON_MINUS = "buttonToggle9"
|
|
||||||
const val PREF_BUTTON_DPAD = "buttonToggle10"
|
|
||||||
const val PREF_STICK_L = "buttonToggle11"
|
|
||||||
const val PREF_STICK_R = "buttonToggle12"
|
|
||||||
const val PREF_BUTTON_STICK_L = "buttonToggle13"
|
|
||||||
const val PREF_BUTTON_STICK_R = "buttonToggle14"
|
|
||||||
const val PREF_BUTTON_HOME = "buttonToggle15"
|
|
||||||
const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
|
|
||||||
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
|
|
||||||
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
|
|
||||||
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
|
|
||||||
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
|
|
||||||
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
|
|
||||||
val overlayPreferences = listOf(
|
|
||||||
PREF_BUTTON_A,
|
|
||||||
PREF_BUTTON_B,
|
|
||||||
PREF_BUTTON_X,
|
|
||||||
PREF_BUTTON_Y,
|
|
||||||
PREF_BUTTON_L,
|
|
||||||
PREF_BUTTON_R,
|
|
||||||
PREF_BUTTON_ZL,
|
|
||||||
PREF_BUTTON_ZR,
|
|
||||||
PREF_BUTTON_PLUS,
|
|
||||||
PREF_BUTTON_MINUS,
|
|
||||||
PREF_BUTTON_DPAD,
|
|
||||||
PREF_STICK_L,
|
|
||||||
PREF_STICK_R,
|
|
||||||
PREF_BUTTON_HOME,
|
|
||||||
PREF_BUTTON_SCREENSHOT,
|
|
||||||
PREF_BUTTON_STICK_L,
|
|
||||||
PREF_BUTTON_STICK_R
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deprecated layout preference keys
|
|
||||||
const val PREF_LANDSCAPE_SUFFIX = "_Landscape"
|
|
||||||
const val PREF_PORTRAIT_SUFFIX = "_Portrait"
|
|
||||||
const val PREF_FOLDABLE_SUFFIX = "_Foldable"
|
|
||||||
val overlayLayoutSuffixes = listOf(
|
|
||||||
PREF_LANDSCAPE_SUFFIX,
|
|
||||||
PREF_PORTRAIT_SUFFIX,
|
|
||||||
PREF_FOLDABLE_SUFFIX
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deprecated theme preference keys
|
|
||||||
const val PREF_THEME = "Theme"
|
|
||||||
const val PREF_THEME_MODE = "ThemeMode"
|
|
||||||
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
|
|
||||||
|
|
||||||
enum class EmulationOrientation(val int: Int) {
|
|
||||||
Unspecified(0),
|
|
||||||
SensorLandscape(5),
|
|
||||||
Landscape(1),
|
|
||||||
ReverseLandscape(2),
|
|
||||||
SensorPortrait(6),
|
|
||||||
Portrait(4),
|
|
||||||
ReversePortrait(3);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(int: Int): EmulationOrientation =
|
|
||||||
entries.firstOrNull { it.int == int } ?: Unspecified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class EmulationVerticalAlignment(val int: Int) {
|
|
||||||
Top(1),
|
|
||||||
Center(0),
|
|
||||||
Bottom(2);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(int: Int): EmulationVerticalAlignment =
|
|
||||||
entries.firstOrNull { it.int == int } ?: Center
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
enum class StringSetting(override val key: String) : AbstractStringSetting {
|
|
||||||
DRIVER_PATH("driver_path"),
|
|
||||||
DEVICE_NAME("device_name");
|
|
||||||
|
|
||||||
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
|
|
||||||
|
|
||||||
override fun setString(value: String) {
|
|
||||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
|
||||||
global = false
|
|
||||||
}
|
|
||||||
NativeConfig.setString(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) }
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
|
|
||||||
|
|
||||||
override fun reset() = NativeConfig.setString(key, defaultValue)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model.view
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.AnalogDirection
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.InputType
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
|
|
||||||
import org.sudachi.sudachi_emu.utils.ParamPackage
|
|
||||||
|
|
||||||
class AnalogInputSetting(
|
|
||||||
override val playerIndex: Int,
|
|
||||||
val nativeAnalog: NativeAnalog,
|
|
||||||
val analogDirection: AnalogDirection,
|
|
||||||
@StringRes titleId: Int = 0,
|
|
||||||
titleString: String = ""
|
|
||||||
) : InputSetting(titleId, titleString) {
|
|
||||||
override val type = TYPE_INPUT
|
|
||||||
override val inputType = InputType.Stick
|
|
||||||
|
|
||||||
override fun getSelectedValue(): String {
|
|
||||||
val params = NativeInput.getStickParam(playerIndex, nativeAnalog)
|
|
||||||
val analog = analogToText(params, analogDirection.param)
|
|
||||||
return getDisplayString(params, analog)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setSelectedValue(param: ParamPackage) =
|
|
||||||
NativeInput.setStickParam(playerIndex, nativeAnalog, param)
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model.view
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractLongSetting
|
|
||||||
|
|
||||||
class DateTimeSetting(
|
|
||||||
private val longSetting: AbstractLongSetting,
|
|
||||||
@StringRes titleId: Int = 0,
|
|
||||||
titleString: String = "",
|
|
||||||
@StringRes descriptionId: Int = 0,
|
|
||||||
descriptionString: String = ""
|
|
||||||
) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {
|
|
||||||
override val type = TYPE_DATETIME_SETTING
|
|
||||||
|
|
||||||
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
|
|
||||||
fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model.view
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.ButtonName
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.InputType
|
|
||||||
import org.sudachi.sudachi_emu.utils.ParamPackage
|
|
||||||
|
|
||||||
sealed class InputSetting(
|
|
||||||
@StringRes titleId: Int,
|
|
||||||
titleString: String
|
|
||||||
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
|
|
||||||
override val type = TYPE_INPUT
|
|
||||||
abstract val inputType: InputType
|
|
||||||
abstract val playerIndex: Int
|
|
||||||
|
|
||||||
protected val context get() = SudachiApplication.appContext
|
|
||||||
|
|
||||||
abstract fun getSelectedValue(): String
|
|
||||||
|
|
||||||
abstract fun setSelectedValue(param: ParamPackage)
|
|
||||||
|
|
||||||
protected fun getDisplayString(params: ParamPackage, control: String): String {
|
|
||||||
val deviceName = params.get("display", "")
|
|
||||||
deviceName.ifEmpty {
|
|
||||||
return context.getString(R.string.not_set)
|
|
||||||
}
|
|
||||||
return "$deviceName: $control"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDirectionName(direction: String): String =
|
|
||||||
when (direction) {
|
|
||||||
"up" -> context.getString(R.string.up)
|
|
||||||
"down" -> context.getString(R.string.down)
|
|
||||||
"left" -> context.getString(R.string.left)
|
|
||||||
"right" -> context.getString(R.string.right)
|
|
||||||
else -> direction
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun buttonToText(param: ParamPackage): String {
|
|
||||||
if (!param.has("engine")) {
|
|
||||||
return context.getString(R.string.not_set)
|
|
||||||
}
|
|
||||||
|
|
||||||
val toggle = if (param.get("toggle", false)) "~" else ""
|
|
||||||
val inverted = if (param.get("inverted", false)) "!" else ""
|
|
||||||
val invert = if (param.get("invert", "+") == "-") "-" else ""
|
|
||||||
val turbo = if (param.get("turbo", false)) "$" else ""
|
|
||||||
val commonButtonName = NativeInput.getButtonName(param)
|
|
||||||
|
|
||||||
if (commonButtonName == ButtonName.Invalid) {
|
|
||||||
return context.getString(R.string.invalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commonButtonName == ButtonName.Engine) {
|
|
||||||
return param.get("engine", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commonButtonName == ButtonName.Value) {
|
|
||||||
if (param.has("hat")) {
|
|
||||||
val hat = getDirectionName(param.get("direction", ""))
|
|
||||||
return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat)
|
|
||||||
}
|
|
||||||
if (param.has("axis")) {
|
|
||||||
val axis = param.get("axis", "")
|
|
||||||
return context.getString(
|
|
||||||
R.string.qualified_button_stick_axis,
|
|
||||||
toggle,
|
|
||||||
inverted,
|
|
||||||
invert,
|
|
||||||
axis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (param.has("button")) {
|
|
||||||
val button = param.get("button", "")
|
|
||||||
return context.getString(R.string.qualified_button, turbo, toggle, inverted, button)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun analogToText(param: ParamPackage, direction: String): String {
|
|
||||||
if (!param.has("engine")) {
|
|
||||||
return context.getString(R.string.not_set)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (param.get("engine", "") == "analog_from_button") {
|
|
||||||
return buttonToText(ParamPackage(param.get(direction, "")))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!param.has("axis_x") || !param.has("axis_y")) {
|
|
||||||
return context.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
val xAxis = param.get("axis_x", "")
|
|
||||||
val yAxis = param.get("axis_y", "")
|
|
||||||
val xInvert = param.get("invert_x", "+") == "-"
|
|
||||||
val yInvert = param.get("invert_y", "+") == "-"
|
|
||||||
|
|
||||||
if (direction == "modifier") {
|
|
||||||
return context.getString(R.string.unused)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (direction) {
|
|
||||||
"up" -> {
|
|
||||||
val yInvertString = if (yInvert) "+" else "-"
|
|
||||||
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
|
|
||||||
}
|
|
||||||
|
|
||||||
"down" -> {
|
|
||||||
val yInvertString = if (yInvert) "-" else "+"
|
|
||||||
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
|
|
||||||
}
|
|
||||||
|
|
||||||
"left" -> {
|
|
||||||
val xInvertString = if (xInvert) "+" else "-"
|
|
||||||
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
|
|
||||||
}
|
|
||||||
|
|
||||||
"right" -> {
|
|
||||||
val xInvertString = if (xInvert) "-" else "+"
|
|
||||||
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model.view
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.InputType
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
|
|
||||||
import org.sudachi.sudachi_emu.utils.ParamPackage
|
|
||||||
|
|
||||||
class ModifierInputSetting(
|
|
||||||
override val playerIndex: Int,
|
|
||||||
val nativeAnalog: NativeAnalog,
|
|
||||||
@StringRes titleId: Int = 0,
|
|
||||||
titleString: String = ""
|
|
||||||
) : InputSetting(titleId, titleString) {
|
|
||||||
override val inputType = InputType.Button
|
|
||||||
|
|
||||||
override fun getSelectedValue(): String {
|
|
||||||
val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
|
|
||||||
val modifierParam = ParamPackage(analogParam.get("modifier", ""))
|
|
||||||
return buttonToText(modifierParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setSelectedValue(param: ParamPackage) {
|
|
||||||
val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
|
|
||||||
newParam.set("modifier", param.serialize())
|
|
||||||
NativeInput.setStickParam(playerIndex, nativeAnalog, newParam)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model.view
|
|
||||||
|
|
||||||
import androidx.annotation.ArrayRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractSetting
|
|
||||||
|
|
||||||
class SingleChoiceSetting(
|
|
||||||
setting: AbstractSetting,
|
|
||||||
@StringRes titleId: Int = 0,
|
|
||||||
titleString: String = "",
|
|
||||||
@StringRes descriptionId: Int = 0,
|
|
||||||
descriptionString: String = "",
|
|
||||||
@ArrayRes val choicesId: Int,
|
|
||||||
@ArrayRes val valuesId: Int
|
|
||||||
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
|
|
||||||
override val type = TYPE_SINGLE_CHOICE
|
|
||||||
|
|
||||||
fun getSelectedValue(needsGlobal: Boolean = false) =
|
|
||||||
when (setting) {
|
|
||||||
is AbstractIntSetting -> setting.getInt(needsGlobal)
|
|
||||||
else -> -1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model.view
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractStringSetting
|
|
||||||
|
|
||||||
class StringInputSetting(
|
|
||||||
setting: AbstractStringSetting,
|
|
||||||
@StringRes titleId: Int = 0,
|
|
||||||
titleString: String = "",
|
|
||||||
@StringRes descriptionId: Int = 0,
|
|
||||||
descriptionString: String = ""
|
|
||||||
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
|
|
||||||
override val type = TYPE_STRING_INPUT
|
|
||||||
|
|
||||||
fun getSelectedValue(needsGlobal: Boolean = false) = setting.getValueAsString(needsGlobal)
|
|
||||||
|
|
||||||
fun setSelectedValue(selection: String) =
|
|
||||||
(setting as AbstractStringSetting).setString(selection)
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.model.view
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractBooleanSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractSetting
|
|
||||||
|
|
||||||
class SwitchSetting(
|
|
||||||
setting: AbstractSetting,
|
|
||||||
@StringRes titleId: Int = 0,
|
|
||||||
titleString: String = "",
|
|
||||||
@StringRes descriptionId: Int = 0,
|
|
||||||
descriptionString: String = ""
|
|
||||||
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
|
|
||||||
override val type = TYPE_SWITCH
|
|
||||||
|
|
||||||
fun getIsChecked(needsGlobal: Boolean = false): Boolean {
|
|
||||||
return when (setting) {
|
|
||||||
is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
|
|
||||||
is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setChecked(value: Boolean) {
|
|
||||||
when (setting) {
|
|
||||||
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
|
|
||||||
is AbstractBooleanSetting -> setting.setBoolean(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,300 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.graphics.drawable.Animatable2
|
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.DialogMappingBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeButton
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.AnalogInputSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.ButtonInputSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.InputSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.ModifierInputSetting
|
|
||||||
import org.sudachi.sudachi_emu.utils.InputHandler
|
|
||||||
import org.sudachi.sudachi_emu.utils.ParamPackage
|
|
||||||
|
|
||||||
class InputDialogFragment : DialogFragment() {
|
|
||||||
private var inputAccepted = false
|
|
||||||
|
|
||||||
private var position: Int = 0
|
|
||||||
|
|
||||||
private lateinit var inputSetting: InputSetting
|
|
||||||
|
|
||||||
private lateinit var binding: DialogMappingBinding
|
|
||||||
|
|
||||||
private val settingsViewModel: SettingsViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
if (settingsViewModel.clickedItem == null) dismiss()
|
|
||||||
|
|
||||||
position = requireArguments().getInt(POSITION)
|
|
||||||
|
|
||||||
InputHandler.updateControllerData()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
inputSetting = settingsViewModel.clickedItem as InputSetting
|
|
||||||
binding = DialogMappingBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setPositiveButton(android.R.string.cancel) { _, _ ->
|
|
||||||
NativeInput.stopMapping()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.setView(binding.root)
|
|
||||||
|
|
||||||
val playButtonMapAnimation = { twoDirections: Boolean ->
|
|
||||||
val stickAnimation: AnimatedVectorDrawable
|
|
||||||
val buttonAnimation: AnimatedVectorDrawable
|
|
||||||
binding.imageStickAnimation.apply {
|
|
||||||
val anim = if (twoDirections) {
|
|
||||||
R.drawable.stick_two_direction_anim
|
|
||||||
} else {
|
|
||||||
R.drawable.stick_one_direction_anim
|
|
||||||
}
|
|
||||||
setBackgroundResource(anim)
|
|
||||||
stickAnimation = background as AnimatedVectorDrawable
|
|
||||||
}
|
|
||||||
binding.imageButtonAnimation.apply {
|
|
||||||
setBackgroundResource(R.drawable.button_anim)
|
|
||||||
buttonAnimation = background as AnimatedVectorDrawable
|
|
||||||
}
|
|
||||||
stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
|
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
|
||||||
buttonAnimation.start()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
|
|
||||||
override fun onAnimationEnd(drawable: Drawable?) {
|
|
||||||
stickAnimation.start()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
stickAnimation.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
when (val setting = inputSetting) {
|
|
||||||
is AnalogInputSetting -> {
|
|
||||||
when (setting.nativeAnalog) {
|
|
||||||
NativeAnalog.LStick -> builder.setTitle(
|
|
||||||
getString(R.string.map_control, getString(R.string.left_stick))
|
|
||||||
)
|
|
||||||
|
|
||||||
NativeAnalog.RStick -> builder.setTitle(
|
|
||||||
getString(R.string.map_control, getString(R.string.right_stick))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setMessage(R.string.stick_map_description)
|
|
||||||
|
|
||||||
playButtonMapAnimation.invoke(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
is ModifierInputSetting -> {
|
|
||||||
builder.setTitle(getString(R.string.map_control, setting.title))
|
|
||||||
.setMessage(R.string.button_map_description)
|
|
||||||
playButtonMapAnimation.invoke(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
is ButtonInputSetting -> {
|
|
||||||
if (setting.nativeButton == NativeButton.DUp ||
|
|
||||||
setting.nativeButton == NativeButton.DDown ||
|
|
||||||
setting.nativeButton == NativeButton.DLeft ||
|
|
||||||
setting.nativeButton == NativeButton.DRight
|
|
||||||
) {
|
|
||||||
builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
|
|
||||||
} else {
|
|
||||||
builder.setTitle(getString(R.string.map_control, setting.title))
|
|
||||||
}
|
|
||||||
builder.setMessage(R.string.button_map_description)
|
|
||||||
playButtonMapAnimation.invoke(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
view.requestFocus()
|
|
||||||
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
|
||||||
dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
|
|
||||||
binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
|
|
||||||
NativeInput.beginMapping(inputSetting.inputType.int)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
|
|
||||||
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val action = when (event.action) {
|
|
||||||
KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
|
|
||||||
KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
val controllerData =
|
|
||||||
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
|
|
||||||
NativeInput.onGamePadButtonEvent(
|
|
||||||
controllerData.getGUID(),
|
|
||||||
controllerData.getPort(),
|
|
||||||
event.keyCode,
|
|
||||||
action
|
|
||||||
)
|
|
||||||
onInputReceived(event.device)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMotionEvent(event: MotionEvent): Boolean {
|
|
||||||
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
|
|
||||||
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temp workaround for DPads that give both axis and button input. The input system can't
|
|
||||||
// take in a specific axis direction for a binding so you lose half of the directions for a DPad.
|
|
||||||
|
|
||||||
val controllerData =
|
|
||||||
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
|
|
||||||
event.device.motionRanges.forEach {
|
|
||||||
NativeInput.onGamePadAxisEvent(
|
|
||||||
controllerData.getGUID(),
|
|
||||||
controllerData.getPort(),
|
|
||||||
it.axis,
|
|
||||||
event.getAxisValue(it.axis)
|
|
||||||
)
|
|
||||||
onInputReceived(event.device)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onInputReceived(device: InputDevice) {
|
|
||||||
val params = ParamPackage(NativeInput.getNextInput())
|
|
||||||
if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
|
|
||||||
inputAccepted = true
|
|
||||||
setResult(params, device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setResult(params: ParamPackage, device: InputDevice) {
|
|
||||||
NativeInput.stopMapping()
|
|
||||||
params.set("display", "${device.name} ${params.get("port", 0)}")
|
|
||||||
when (val item = settingsViewModel.clickedItem as InputSetting) {
|
|
||||||
is ModifierInputSetting,
|
|
||||||
is ButtonInputSetting -> {
|
|
||||||
// Invert DPad up and left bindings by default
|
|
||||||
val tempSetting = inputSetting as? ButtonInputSetting
|
|
||||||
if (tempSetting != null) {
|
|
||||||
if (tempSetting.nativeButton == NativeButton.DUp ||
|
|
||||||
tempSetting.nativeButton == NativeButton.DLeft &&
|
|
||||||
params.has("axis")
|
|
||||||
) {
|
|
||||||
params.set("invert", "-")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item.setSelectedValue(params)
|
|
||||||
settingsViewModel.setAdapterItemChanged(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
is AnalogInputSetting -> {
|
|
||||||
var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
|
|
||||||
analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
|
|
||||||
|
|
||||||
// Invert Y-Axis by default
|
|
||||||
analogParam.set("invert_y", "-")
|
|
||||||
|
|
||||||
item.setSelectedValue(analogParam)
|
|
||||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun adjustAnalogParam(
|
|
||||||
inputParam: ParamPackage,
|
|
||||||
analogParam: ParamPackage,
|
|
||||||
buttonName: String
|
|
||||||
): ParamPackage {
|
|
||||||
// The poller returned a complete axis, so set all the buttons
|
|
||||||
if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
|
|
||||||
return inputParam
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the current configuration has either no engine or an axis binding.
|
|
||||||
// Clears out the old binding and adds one with analog_from_button.
|
|
||||||
if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
|
|
||||||
analogParam.clear()
|
|
||||||
analogParam.set("engine", "analog_from_button")
|
|
||||||
}
|
|
||||||
analogParam.set(buttonName, inputParam.serialize())
|
|
||||||
return analogParam
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isInputAcceptable(params: ParamPackage): Boolean {
|
|
||||||
if (InputHandler.registeredControllers.size == 1) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.has("motion")) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
|
|
||||||
if (currentDevice.get("engine", "any") == "any") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
|
|
||||||
params.get("guid", "") == currentDevice.get("guid2", "")
|
|
||||||
return params.get("engine", "") == currentDevice.get("engine", "") &&
|
|
||||||
guidMatch &&
|
|
||||||
params.get("port", 0) == currentDevice.get("port", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "InputDialogFragment"
|
|
||||||
|
|
||||||
const val POSITION = "Position"
|
|
||||||
|
|
||||||
fun newInstance(
|
|
||||||
inputMappingViewModel: SettingsViewModel,
|
|
||||||
setting: InputSetting,
|
|
||||||
position: Int
|
|
||||||
): InputDialogFragment {
|
|
||||||
inputMappingViewModel.clickedItem = setting
|
|
||||||
val args = Bundle()
|
|
||||||
args.putInt(POSITION, position)
|
|
||||||
val fragment = InputDialogFragment()
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import androidx.navigation.navArgs
|
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import java.io.IOException
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ActivitySettingsBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.utils.SettingsFile
|
|
||||||
import org.sudachi.sudachi_emu.fragments.ResetSettingsDialogFragment
|
|
||||||
import org.sudachi.sudachi_emu.utils.*
|
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
|
||||||
private lateinit var binding: ActivitySettingsBinding
|
|
||||||
|
|
||||||
private val args by navArgs<SettingsActivityArgs>()
|
|
||||||
|
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
ThemeHelper.setTheme(this)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
|
|
||||||
SettingsFile.loadCustomConfig(args.game!!)
|
|
||||||
}
|
|
||||||
settingsViewModel.game = args.game
|
|
||||||
|
|
||||||
val navHostFragment =
|
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
|
||||||
navHostFragment.navController.setGraph(R.navigation.settings_navigation, intent.extras)
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
|
|
||||||
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
|
||||||
InsetsHelper.GESTURE_NAVIGATION
|
|
||||||
) {
|
|
||||||
binding.navigationBarShade.setBackgroundColor(
|
|
||||||
ThemeHelper.getColorWithOpacity(
|
|
||||||
MaterialColors.getColor(
|
|
||||||
binding.navigationBarShade,
|
|
||||||
com.google.android.material.R.attr.colorSurface
|
|
||||||
),
|
|
||||||
ThemeHelper.SYSTEM_BAR_ALPHA
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsViewModel.shouldRecreate.collect(
|
|
||||||
this,
|
|
||||||
resetState = { settingsViewModel.setShouldRecreate(false) }
|
|
||||||
) { if (it) recreate() }
|
|
||||||
settingsViewModel.shouldNavigateBack.collect(
|
|
||||||
this,
|
|
||||||
resetState = { settingsViewModel.setShouldNavigateBack(false) }
|
|
||||||
) { if (it) navigateBack() }
|
|
||||||
settingsViewModel.shouldShowResetSettingsDialog.collect(
|
|
||||||
this,
|
|
||||||
resetState = { settingsViewModel.setShouldShowResetSettingsDialog(false) }
|
|
||||||
) {
|
|
||||||
if (it) {
|
|
||||||
ResetSettingsDialogFragment().show(
|
|
||||||
supportFragmentManager,
|
|
||||||
ResetSettingsDialogFragment.TAG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(
|
|
||||||
this,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() = navigateBack()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun navigateBack() {
|
|
||||||
val navHostFragment =
|
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
|
||||||
if (navHostFragment.childFragmentManager.backStackEntryCount > 0) {
|
|
||||||
navHostFragment.navController.popBackStack()
|
|
||||||
} else {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
if (!DirectoryInitialization.areDirectoriesReady) {
|
|
||||||
DirectoryInitialization.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
|
||||||
if (isFinishing) {
|
|
||||||
NativeInput.reloadInputDevices()
|
|
||||||
NativeLibrary.applySettings()
|
|
||||||
if (args.game == null) {
|
|
||||||
NativeConfig.saveGlobalConfig()
|
|
||||||
} else if (NativeConfig.isPerGameConfigLoaded()) {
|
|
||||||
NativeLibrary.logSettings()
|
|
||||||
NativeConfig.savePerGameConfig()
|
|
||||||
NativeConfig.unloadPerGameConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSettingsReset() {
|
|
||||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
|
||||||
if (args.game == null) {
|
|
||||||
NativeConfig.unloadGlobalConfig()
|
|
||||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
|
||||||
if (!settingsFile.delete()) {
|
|
||||||
throw IOException("Failed to delete $settingsFile")
|
|
||||||
}
|
|
||||||
NativeConfig.initializeGlobalConfig()
|
|
||||||
} else {
|
|
||||||
NativeConfig.unloadPerGameConfig()
|
|
||||||
val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
|
|
||||||
if (!settingsFile.delete()) {
|
|
||||||
throw IOException("Failed to delete $settingsFile")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.settings_reset),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.navigationBarShade
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
|
|
||||||
// The only situation where we care to have a nav bar shade is when it's at the bottom
|
|
||||||
// of the screen where scrolling list elements can go behind it.
|
|
||||||
val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
|
|
||||||
mlpNavShade.height = barInsets.bottom
|
|
||||||
binding.navigationBarShade.layoutParams = mlpNavShade
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,975 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
|
||||||
import android.widget.Toast
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.AnalogDirection
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeAnalog
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeButton
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NpadStyleIndex
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractBooleanSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.AbstractIntSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.BooleanSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.ByteSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.IntSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.LongSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.Settings
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.Settings.MenuTag
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.ShortSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.StringSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.*
|
|
||||||
import org.sudachi.sudachi_emu.utils.InputHandler
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
class SettingsFragmentPresenter(
|
|
||||||
private val settingsViewModel: SettingsViewModel,
|
|
||||||
private val adapter: SettingsAdapter,
|
|
||||||
private var menuTag: MenuTag
|
|
||||||
) {
|
|
||||||
private var settingsList = ArrayList<SettingsItem>()
|
|
||||||
|
|
||||||
private val context get() = SudachiApplication.appContext
|
|
||||||
|
|
||||||
// Extension for altering settings list based on each setting's properties
|
|
||||||
fun ArrayList<SettingsItem>.add(key: String) {
|
|
||||||
val item = SettingsItem.settingsItems[key]!!
|
|
||||||
if (settingsViewModel.game != null && !item.setting.isSwitchable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
|
|
||||||
item.setting.global = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val pairedSettingKey = item.setting.pairedSettingKey
|
|
||||||
if (pairedSettingKey.isNotEmpty()) {
|
|
||||||
val pairedSettingValue = NativeConfig.getBoolean(
|
|
||||||
pairedSettingKey,
|
|
||||||
if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) {
|
|
||||||
!NativeConfig.usingGlobal(pairedSettingKey)
|
|
||||||
} else {
|
|
||||||
NativeConfig.usingGlobal(pairedSettingKey)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (!pairedSettingValue) return
|
|
||||||
}
|
|
||||||
add(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows you to show/hide abstract settings based on the paired setting key
|
|
||||||
fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) {
|
|
||||||
val pairedSettingKey = item.setting.pairedSettingKey
|
|
||||||
if (pairedSettingKey.isNotEmpty()) {
|
|
||||||
val pairedSettingsItem =
|
|
||||||
this.firstOrNull { it.setting.key == pairedSettingKey } ?: return
|
|
||||||
val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting
|
|
||||||
if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return
|
|
||||||
}
|
|
||||||
add(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onViewCreated() {
|
|
||||||
loadSettingsList()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
fun loadSettingsList(notifyDataSetChanged: Boolean = false) {
|
|
||||||
val sl = ArrayList<SettingsItem>()
|
|
||||||
when (menuTag) {
|
|
||||||
MenuTag.SECTION_ROOT -> addConfigSettings(sl)
|
|
||||||
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
|
|
||||||
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
|
|
||||||
MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
|
|
||||||
MenuTag.SECTION_INPUT -> addInputSettings(sl)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6)
|
|
||||||
MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
|
|
||||||
MenuTag.SECTION_THEME -> addThemeSettings(sl)
|
|
||||||
MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
|
|
||||||
}
|
|
||||||
settingsList = sl
|
|
||||||
adapter.submitList(settingsList) {
|
|
||||||
if (notifyDataSetChanged) {
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
|
|
||||||
sl.apply {
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleId = R.string.preferences_system,
|
|
||||||
descriptionId = R.string.preferences_system_description,
|
|
||||||
iconId = R.drawable.ic_system_settings,
|
|
||||||
menuKey = MenuTag.SECTION_SYSTEM
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleId = R.string.preferences_graphics,
|
|
||||||
descriptionId = R.string.preferences_graphics_description,
|
|
||||||
iconId = R.drawable.ic_graphics,
|
|
||||||
menuKey = MenuTag.SECTION_RENDERER
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleId = R.string.preferences_audio,
|
|
||||||
descriptionId = R.string.preferences_audio_description,
|
|
||||||
iconId = R.drawable.ic_audio,
|
|
||||||
menuKey = MenuTag.SECTION_AUDIO
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleId = R.string.preferences_debug,
|
|
||||||
descriptionId = R.string.preferences_debug_description,
|
|
||||||
iconId = R.drawable.ic_code,
|
|
||||||
menuKey = MenuTag.SECTION_DEBUG
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
RunnableSetting(
|
|
||||||
titleId = R.string.reset_to_default,
|
|
||||||
descriptionId = R.string.reset_to_default_description,
|
|
||||||
isRunnable = !NativeLibrary.isRunning(),
|
|
||||||
iconId = R.drawable.ic_restore
|
|
||||||
) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
|
|
||||||
sl.apply {
|
|
||||||
add(StringSetting.DEVICE_NAME.key)
|
|
||||||
add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
|
|
||||||
add(ShortSetting.RENDERER_SPEED_LIMIT.key)
|
|
||||||
add(BooleanSetting.USE_DOCKED_MODE.key)
|
|
||||||
add(IntSetting.REGION_INDEX.key)
|
|
||||||
add(IntSetting.LANGUAGE_INDEX.key)
|
|
||||||
add(BooleanSetting.USE_CUSTOM_RTC.key)
|
|
||||||
add(LongSetting.CUSTOM_RTC.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
|
|
||||||
sl.apply {
|
|
||||||
add(IntSetting.RENDERER_ACCURACY.key)
|
|
||||||
add(IntSetting.RENDERER_RESOLUTION.key)
|
|
||||||
add(IntSetting.RENDERER_VSYNC.key)
|
|
||||||
add(IntSetting.RENDERER_SCALING_FILTER.key)
|
|
||||||
add(IntSetting.FSR_SHARPENING_SLIDER.key)
|
|
||||||
add(IntSetting.RENDERER_ANTI_ALIASING.key)
|
|
||||||
add(IntSetting.MAX_ANISOTROPY.key)
|
|
||||||
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
|
|
||||||
add(IntSetting.RENDERER_ASPECT_RATIO.key)
|
|
||||||
add(IntSetting.VERTICAL_ALIGNMENT.key)
|
|
||||||
add(BooleanSetting.PICTURE_IN_PICTURE.key)
|
|
||||||
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
|
|
||||||
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
|
|
||||||
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
|
|
||||||
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
|
|
||||||
sl.apply {
|
|
||||||
add(IntSetting.AUDIO_OUTPUT_ENGINE.key)
|
|
||||||
add(ByteSetting.AUDIO_VOLUME.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addInputSettings(sl: ArrayList<SettingsItem>) {
|
|
||||||
settingsViewModel.currentDevice = 0
|
|
||||||
|
|
||||||
if (NativeConfig.isPerGameConfigLoaded()) {
|
|
||||||
NativeInput.loadInputProfiles()
|
|
||||||
val profiles = NativeInput.getInputProfileNames().toMutableList()
|
|
||||||
profiles.add(0, "")
|
|
||||||
val prettyProfiles = profiles.toTypedArray()
|
|
||||||
prettyProfiles[0] =
|
|
||||||
context.getString(R.string.use_global_input_configuration)
|
|
||||||
sl.apply {
|
|
||||||
for (i in 0 until 8) {
|
|
||||||
add(
|
|
||||||
IntSingleChoiceSetting(
|
|
||||||
getPerGameProfileSetting(profiles, i),
|
|
||||||
titleString = getPlayerProfileString(i + 1),
|
|
||||||
choices = prettyProfiles,
|
|
||||||
values = IntArray(profiles.size) { it }.toTypedArray()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val getConnectedIcon: (Int) -> Int = { playerIndex: Int ->
|
|
||||||
if (NativeInput.getIsConnected(playerIndex)) {
|
|
||||||
R.drawable.ic_controller
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_controller_disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val inputSettings = NativeConfig.getInputSettings(true)
|
|
||||||
sl.apply {
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(1),
|
|
||||||
descriptionString = inputSettings[0].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE,
|
|
||||||
iconId = getConnectedIcon(0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(2),
|
|
||||||
descriptionString = inputSettings[1].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO,
|
|
||||||
iconId = getConnectedIcon(1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(3),
|
|
||||||
descriptionString = inputSettings[2].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE,
|
|
||||||
iconId = getConnectedIcon(2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(4),
|
|
||||||
descriptionString = inputSettings[3].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR,
|
|
||||||
iconId = getConnectedIcon(3)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(5),
|
|
||||||
descriptionString = inputSettings[4].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE,
|
|
||||||
iconId = getConnectedIcon(4)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(6),
|
|
||||||
descriptionString = inputSettings[5].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX,
|
|
||||||
iconId = getConnectedIcon(5)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(7),
|
|
||||||
descriptionString = inputSettings[6].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN,
|
|
||||||
iconId = getConnectedIcon(6)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
SubmenuSetting(
|
|
||||||
titleString = Settings.getPlayerString(8),
|
|
||||||
descriptionString = inputSettings[7].profileName,
|
|
||||||
menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT,
|
|
||||||
iconId = getConnectedIcon(7)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPlayerProfileString(player: Int): String =
|
|
||||||
context.getString(R.string.player_num_profile, player)
|
|
||||||
|
|
||||||
private fun getPerGameProfileSetting(
|
|
||||||
profiles: List<String>,
|
|
||||||
playerIndex: Int
|
|
||||||
): AbstractIntSetting {
|
|
||||||
return object : AbstractIntSetting {
|
|
||||||
private val players
|
|
||||||
get() = NativeConfig.getInputSettings(false)
|
|
||||||
|
|
||||||
override val key = ""
|
|
||||||
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int {
|
|
||||||
val currentProfile = players[playerIndex].profileName
|
|
||||||
profiles.forEachIndexed { i, profile ->
|
|
||||||
if (profile == currentProfile) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value])
|
|
||||||
NativeInput.connectControllers(playerIndex)
|
|
||||||
NativeConfig.saveControlPlayerValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = 0
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
|
|
||||||
|
|
||||||
override fun reset() = setInt(defaultValue)
|
|
||||||
|
|
||||||
override var global = true
|
|
||||||
|
|
||||||
override val isRuntimeModifiable = true
|
|
||||||
|
|
||||||
override val isSaveable = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) {
|
|
||||||
sl.apply {
|
|
||||||
val connectedSetting = object : AbstractBooleanSetting {
|
|
||||||
override val key = "connected"
|
|
||||||
|
|
||||||
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
|
||||||
NativeInput.getIsConnected(playerIndex)
|
|
||||||
|
|
||||||
override fun setBoolean(value: Boolean) =
|
|
||||||
NativeInput.connectControllers(playerIndex, value)
|
|
||||||
|
|
||||||
override val defaultValue = playerIndex == 0
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
getBoolean(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = setBoolean(defaultValue)
|
|
||||||
}
|
|
||||||
add(SwitchSetting(connectedSetting, R.string.connected))
|
|
||||||
|
|
||||||
val styleTags = NativeInput.getSupportedStyleTags(playerIndex)
|
|
||||||
val npadType = object : AbstractIntSetting {
|
|
||||||
override val key = "npad_type"
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int {
|
|
||||||
val styleIndex = NativeInput.getStyleIndex(playerIndex)
|
|
||||||
return styleTags.indexOfFirst { it == styleIndex }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
NativeInput.setStyleIndex(playerIndex, styleTags[value])
|
|
||||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = NpadStyleIndex.Fullkey.int
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
|
|
||||||
override fun reset() = setInt(defaultValue)
|
|
||||||
override val pairedSettingKey: String = "connected"
|
|
||||||
}
|
|
||||||
addAbstract(
|
|
||||||
IntSingleChoiceSetting(
|
|
||||||
npadType,
|
|
||||||
titleId = R.string.controller_type,
|
|
||||||
choices = styleTags.map { context.getString(it.nameId) }
|
|
||||||
.toTypedArray(),
|
|
||||||
values = IntArray(styleTags.size) { it }.toTypedArray()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
InputHandler.updateControllerData()
|
|
||||||
|
|
||||||
val autoMappingSetting = object : AbstractIntSetting {
|
|
||||||
override val key = "auto_mapping_device"
|
|
||||||
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int = -1
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
val registeredController = InputHandler.registeredControllers[value + 1]
|
|
||||||
val displayName = registeredController.get(
|
|
||||||
"display",
|
|
||||||
context.getString(R.string.unknown)
|
|
||||||
)
|
|
||||||
NativeInput.updateMappingsWithDefault(
|
|
||||||
playerIndex,
|
|
||||||
registeredController,
|
|
||||||
displayName
|
|
||||||
)
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.attempted_auto_map, displayName),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = -1
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
|
|
||||||
|
|
||||||
override fun reset() = setInt(defaultValue)
|
|
||||||
|
|
||||||
override val isRuntimeModifiable: Boolean = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val unknownString = context.getString(R.string.unknown)
|
|
||||||
val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull {
|
|
||||||
val port = it.get("port", -1)
|
|
||||||
return@mapNotNull if (port == 100 || port == -1) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
it.get("display", unknownString)
|
|
||||||
}
|
|
||||||
}.toTypedArray()
|
|
||||||
add(
|
|
||||||
IntSingleChoiceSetting(
|
|
||||||
autoMappingSetting,
|
|
||||||
titleId = R.string.auto_map,
|
|
||||||
descriptionId = R.string.auto_map_description,
|
|
||||||
choices = prettyAutoMappingControllerList,
|
|
||||||
values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val mappingFilterSetting = object : AbstractIntSetting {
|
|
||||||
override val key = "mapping_filter"
|
|
||||||
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
settingsViewModel.currentDevice = value
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = 0
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
|
|
||||||
|
|
||||||
override fun reset() = setInt(defaultValue)
|
|
||||||
|
|
||||||
override val isRuntimeModifiable: Boolean = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val prettyControllerList = InputHandler.registeredControllers.mapNotNull {
|
|
||||||
return@mapNotNull if (it.get("port", 0) == 100) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
it.get("display", unknownString)
|
|
||||||
}
|
|
||||||
}.toTypedArray()
|
|
||||||
add(
|
|
||||||
IntSingleChoiceSetting(
|
|
||||||
mappingFilterSetting,
|
|
||||||
titleId = R.string.input_mapping_filter,
|
|
||||||
descriptionId = R.string.input_mapping_filter_description,
|
|
||||||
choices = prettyControllerList,
|
|
||||||
values = IntArray(prettyControllerList.size) { it }.toTypedArray()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
add(InputProfileSetting(playerIndex))
|
|
||||||
add(
|
|
||||||
RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) {
|
|
||||||
settingsViewModel.setShouldShowResetInputDialog(true)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val styleIndex = NativeInput.getStyleIndex(playerIndex)
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
when (styleIndex) {
|
|
||||||
NpadStyleIndex.Fullkey,
|
|
||||||
NpadStyleIndex.Handheld,
|
|
||||||
NpadStyleIndex.JoyconDual -> {
|
|
||||||
add(HeaderSetting(R.string.buttons))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.Capture,
|
|
||||||
R.string.button_capture
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.JoyconLeft -> {
|
|
||||||
add(HeaderSetting(R.string.buttons))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.Capture,
|
|
||||||
R.string.button_capture
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.JoyconRight -> {
|
|
||||||
add(HeaderSetting(R.string.buttons))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.GameCube -> {
|
|
||||||
add(HeaderSetting(R.string.buttons))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (styleIndex) {
|
|
||||||
NpadStyleIndex.Fullkey,
|
|
||||||
NpadStyleIndex.Handheld,
|
|
||||||
NpadStyleIndex.JoyconDual,
|
|
||||||
NpadStyleIndex.JoyconLeft -> {
|
|
||||||
add(HeaderSetting(R.string.dpad))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Left stick
|
|
||||||
when (styleIndex) {
|
|
||||||
NpadStyleIndex.Fullkey,
|
|
||||||
NpadStyleIndex.Handheld,
|
|
||||||
NpadStyleIndex.JoyconDual,
|
|
||||||
NpadStyleIndex.JoyconLeft -> {
|
|
||||||
add(HeaderSetting(R.string.left_stick))
|
|
||||||
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed))
|
|
||||||
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.GameCube -> {
|
|
||||||
add(HeaderSetting(R.string.control_stick))
|
|
||||||
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
|
|
||||||
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right stick
|
|
||||||
when (styleIndex) {
|
|
||||||
NpadStyleIndex.Fullkey,
|
|
||||||
NpadStyleIndex.Handheld,
|
|
||||||
NpadStyleIndex.JoyconDual,
|
|
||||||
NpadStyleIndex.JoyconRight -> {
|
|
||||||
add(HeaderSetting(R.string.right_stick))
|
|
||||||
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed))
|
|
||||||
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.GameCube -> {
|
|
||||||
add(HeaderSetting(R.string.c_stick))
|
|
||||||
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
|
|
||||||
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// L/R, ZL/ZR, and SL/SR
|
|
||||||
when (styleIndex) {
|
|
||||||
NpadStyleIndex.Fullkey,
|
|
||||||
NpadStyleIndex.Handheld -> {
|
|
||||||
add(HeaderSetting(R.string.triggers))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.JoyconDual -> {
|
|
||||||
add(HeaderSetting(R.string.triggers))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SLLeft,
|
|
||||||
R.string.button_sl_left
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SRLeft,
|
|
||||||
R.string.button_sr_left
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SLRight,
|
|
||||||
R.string.button_sl_right
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SRRight,
|
|
||||||
R.string.button_sr_right
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.JoyconLeft -> {
|
|
||||||
add(HeaderSetting(R.string.triggers))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SLLeft,
|
|
||||||
R.string.button_sl_left
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SRLeft,
|
|
||||||
R.string.button_sr_left
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.JoyconRight -> {
|
|
||||||
add(HeaderSetting(R.string.triggers))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SLRight,
|
|
||||||
R.string.button_sl_right
|
|
||||||
)
|
|
||||||
)
|
|
||||||
add(
|
|
||||||
ButtonInputSetting(
|
|
||||||
playerIndex,
|
|
||||||
NativeButton.SRRight,
|
|
||||||
R.string.button_sr_right
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NpadStyleIndex.GameCube -> {
|
|
||||||
add(HeaderSetting(R.string.triggers))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l))
|
|
||||||
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add(HeaderSetting(R.string.vibration))
|
|
||||||
val vibrationEnabledSetting = object : AbstractBooleanSetting {
|
|
||||||
override val key = "vibration"
|
|
||||||
|
|
||||||
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
|
||||||
NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled
|
|
||||||
|
|
||||||
override fun setBoolean(value: Boolean) {
|
|
||||||
val settings = NativeConfig.getInputSettings(true)
|
|
||||||
settings[playerIndex].vibrationEnabled = value
|
|
||||||
NativeConfig.setInputSettings(settings, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = true
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
getBoolean(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = setBoolean(defaultValue)
|
|
||||||
}
|
|
||||||
add(SwitchSetting(vibrationEnabledSetting, R.string.vibration))
|
|
||||||
|
|
||||||
val useSystemVibratorSetting = object : AbstractBooleanSetting {
|
|
||||||
override val key = ""
|
|
||||||
|
|
||||||
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
|
||||||
NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator
|
|
||||||
|
|
||||||
override fun setBoolean(value: Boolean) {
|
|
||||||
val settings = NativeConfig.getInputSettings(true)
|
|
||||||
settings[playerIndex].useSystemVibrator = value
|
|
||||||
NativeConfig.setInputSettings(settings, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = playerIndex == 0
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
getBoolean(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = setBoolean(defaultValue)
|
|
||||||
|
|
||||||
override val pairedSettingKey: String = "vibration"
|
|
||||||
}
|
|
||||||
addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator))
|
|
||||||
|
|
||||||
val vibrationStrengthSetting = object : AbstractIntSetting {
|
|
||||||
override val key = ""
|
|
||||||
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int =
|
|
||||||
NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
val settings = NativeConfig.getInputSettings(true)
|
|
||||||
settings[playerIndex].vibrationStrength = value
|
|
||||||
NativeConfig.setInputSettings(settings, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = 100
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
getInt(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = setInt(defaultValue)
|
|
||||||
|
|
||||||
override val pairedSettingKey: String = "vibration"
|
|
||||||
}
|
|
||||||
addAbstract(
|
|
||||||
SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones
|
|
||||||
private fun getStickIntSettingFromParam(
|
|
||||||
playerIndex: Int,
|
|
||||||
paramName: String,
|
|
||||||
stick: NativeAnalog,
|
|
||||||
defaultValue: Float
|
|
||||||
): AbstractIntSetting =
|
|
||||||
object : AbstractIntSetting {
|
|
||||||
val params get() = NativeInput.getStickParam(playerIndex, stick)
|
|
||||||
|
|
||||||
override val key = ""
|
|
||||||
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int =
|
|
||||||
(params.get(paramName, defaultValue) * 100).toInt()
|
|
||||||
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
val tempParams = params
|
|
||||||
tempParams.set(paramName, value.toFloat() / 100)
|
|
||||||
NativeInput.setStickParam(playerIndex, stick, tempParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val defaultValue = (defaultValue * 100).toInt()
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
getInt(needsGlobal).toString()
|
|
||||||
|
|
||||||
override fun reset() = setInt(this.defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getExtraStickSettings(
|
|
||||||
playerIndex: Int,
|
|
||||||
nativeAnalog: NativeAnalog
|
|
||||||
): List<SettingsItem> {
|
|
||||||
val stickIsController =
|
|
||||||
NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog))
|
|
||||||
val modifierRangeSetting =
|
|
||||||
getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 0.5f)
|
|
||||||
val stickRangeSetting =
|
|
||||||
getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 0.95f)
|
|
||||||
val stickDeadzoneSetting =
|
|
||||||
getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 0.15f)
|
|
||||||
|
|
||||||
val out = mutableListOf<SettingsItem>().apply {
|
|
||||||
if (stickIsController) {
|
|
||||||
add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150))
|
|
||||||
add(SliderSetting(stickDeadzoneSetting, R.string.deadzone))
|
|
||||||
} else {
|
|
||||||
add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier))
|
|
||||||
add(SliderSetting(modifierRangeSetting, R.string.modifier_range))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> =
|
|
||||||
listOf(
|
|
||||||
AnalogInputSetting(
|
|
||||||
player,
|
|
||||||
stick,
|
|
||||||
AnalogDirection.Up,
|
|
||||||
R.string.up
|
|
||||||
),
|
|
||||||
AnalogInputSetting(
|
|
||||||
player,
|
|
||||||
stick,
|
|
||||||
AnalogDirection.Down,
|
|
||||||
R.string.down
|
|
||||||
),
|
|
||||||
AnalogInputSetting(
|
|
||||||
player,
|
|
||||||
stick,
|
|
||||||
AnalogDirection.Left,
|
|
||||||
R.string.left
|
|
||||||
),
|
|
||||||
AnalogInputSetting(
|
|
||||||
player,
|
|
||||||
stick,
|
|
||||||
AnalogDirection.Right,
|
|
||||||
R.string.right
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
|
|
||||||
sl.apply {
|
|
||||||
val theme: AbstractIntSetting = object : AbstractIntSetting {
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME.getInt()
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
IntSetting.THEME.setInt(value)
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val key: String = IntSetting.THEME.key
|
|
||||||
override val isRuntimeModifiable: Boolean = IntSetting.THEME.isRuntimeModifiable
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
IntSetting.THEME.getValueAsString()
|
|
||||||
|
|
||||||
override val defaultValue: Int = IntSetting.THEME.defaultValue
|
|
||||||
override fun reset() = IntSetting.THEME.setInt(defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
add(
|
|
||||||
SingleChoiceSetting(
|
|
||||||
theme,
|
|
||||||
titleId = R.string.change_app_theme,
|
|
||||||
choicesId = R.array.themeEntriesA12,
|
|
||||||
valuesId = R.array.themeValuesA12
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
add(
|
|
||||||
SingleChoiceSetting(
|
|
||||||
theme,
|
|
||||||
titleId = R.string.change_app_theme,
|
|
||||||
choicesId = R.array.themeEntries,
|
|
||||||
valuesId = R.array.themeValues
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val themeMode: AbstractIntSetting = object : AbstractIntSetting {
|
|
||||||
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME_MODE.getInt()
|
|
||||||
override fun setInt(value: Int) {
|
|
||||||
IntSetting.THEME_MODE.setInt(value)
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val key: String = IntSetting.THEME_MODE.key
|
|
||||||
override val isRuntimeModifiable: Boolean =
|
|
||||||
IntSetting.THEME_MODE.isRuntimeModifiable
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
IntSetting.THEME_MODE.getValueAsString()
|
|
||||||
|
|
||||||
override val defaultValue: Int = IntSetting.THEME_MODE.defaultValue
|
|
||||||
override fun reset() {
|
|
||||||
IntSetting.THEME_MODE.setInt(defaultValue)
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add(
|
|
||||||
SingleChoiceSetting(
|
|
||||||
themeMode,
|
|
||||||
titleId = R.string.change_theme_mode,
|
|
||||||
choicesId = R.array.themeModeEntries,
|
|
||||||
valuesId = R.array.themeModeValues
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
|
|
||||||
override fun getBoolean(needsGlobal: Boolean): Boolean =
|
|
||||||
BooleanSetting.BLACK_BACKGROUNDS.getBoolean()
|
|
||||||
|
|
||||||
override fun setBoolean(value: Boolean) {
|
|
||||||
BooleanSetting.BLACK_BACKGROUNDS.setBoolean(value)
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val key: String = BooleanSetting.BLACK_BACKGROUNDS.key
|
|
||||||
override val isRuntimeModifiable: Boolean =
|
|
||||||
BooleanSetting.BLACK_BACKGROUNDS.isRuntimeModifiable
|
|
||||||
|
|
||||||
override fun getValueAsString(needsGlobal: Boolean): String =
|
|
||||||
BooleanSetting.BLACK_BACKGROUNDS.getValueAsString()
|
|
||||||
|
|
||||||
override val defaultValue: Boolean = BooleanSetting.BLACK_BACKGROUNDS.defaultValue
|
|
||||||
override fun reset() {
|
|
||||||
BooleanSetting.BLACK_BACKGROUNDS
|
|
||||||
.setBoolean(BooleanSetting.BLACK_BACKGROUNDS.defaultValue)
|
|
||||||
settingsViewModel.setShouldRecreate(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add(
|
|
||||||
SwitchSetting(
|
|
||||||
blackBackgrounds,
|
|
||||||
titleId = R.string.use_black_backgrounds,
|
|
||||||
descriptionId = R.string.use_black_backgrounds_description
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
|
|
||||||
sl.apply {
|
|
||||||
add(HeaderSetting(R.string.gpu))
|
|
||||||
add(IntSetting.RENDERER_BACKEND.key)
|
|
||||||
add(BooleanSetting.RENDERER_DEBUG.key)
|
|
||||||
|
|
||||||
add(HeaderSetting(R.string.cpu))
|
|
||||||
add(IntSetting.CPU_BACKEND.key)
|
|
||||||
add(IntSetting.CPU_ACCURACY.key)
|
|
||||||
add(BooleanSetting.CPU_DEBUG_MODE.key)
|
|
||||||
add(SettingsItem.FASTMEM_COMBINED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
|
|
||||||
import org.sudachi.sudachi_emu.model.Game
|
|
||||||
import org.sudachi.sudachi_emu.utils.InputHandler
|
|
||||||
import org.sudachi.sudachi_emu.utils.ParamPackage
|
|
||||||
|
|
||||||
class SettingsViewModel : ViewModel() {
|
|
||||||
var game: Game? = null
|
|
||||||
|
|
||||||
var clickedItem: SettingsItem? = null
|
|
||||||
|
|
||||||
var currentDevice = 0
|
|
||||||
|
|
||||||
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
|
||||||
private val _shouldRecreate = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val shouldNavigateBack: StateFlow<Boolean> get() = _shouldNavigateBack
|
|
||||||
private val _shouldNavigateBack = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val shouldShowResetSettingsDialog: StateFlow<Boolean> get() = _shouldShowResetSettingsDialog
|
|
||||||
private val _shouldShowResetSettingsDialog = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val shouldReloadSettingsList: StateFlow<Boolean> get() = _shouldReloadSettingsList
|
|
||||||
private val _shouldReloadSettingsList = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val sliderProgress: StateFlow<Int> get() = _sliderProgress
|
|
||||||
private val _sliderProgress = MutableStateFlow(-1)
|
|
||||||
|
|
||||||
val sliderTextValue: StateFlow<String> get() = _sliderTextValue
|
|
||||||
private val _sliderTextValue = MutableStateFlow("")
|
|
||||||
|
|
||||||
val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged
|
|
||||||
private val _adapterItemChanged = MutableStateFlow(-1)
|
|
||||||
|
|
||||||
private val _datasetChanged = MutableStateFlow(false)
|
|
||||||
val datasetChanged = _datasetChanged.asStateFlow()
|
|
||||||
|
|
||||||
private val _reloadListAndNotifyDataset = MutableStateFlow(false)
|
|
||||||
val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow()
|
|
||||||
|
|
||||||
private val _shouldShowDeleteProfileDialog = MutableStateFlow("")
|
|
||||||
val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow()
|
|
||||||
|
|
||||||
private val _shouldShowResetInputDialog = MutableStateFlow(false)
|
|
||||||
val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow()
|
|
||||||
|
|
||||||
fun setShouldRecreate(value: Boolean) {
|
|
||||||
_shouldRecreate.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldNavigateBack(value: Boolean) {
|
|
||||||
_shouldNavigateBack.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldShowResetSettingsDialog(value: Boolean) {
|
|
||||||
_shouldShowResetSettingsDialog.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldReloadSettingsList(value: Boolean) {
|
|
||||||
_shouldReloadSettingsList.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSliderTextValue(value: Float, units: String) {
|
|
||||||
_sliderProgress.value = value.toInt()
|
|
||||||
_sliderTextValue.value = String.format(
|
|
||||||
SudachiApplication.appContext.getString(R.string.value_with_units),
|
|
||||||
value.toInt().toString(),
|
|
||||||
units
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSliderProgress(value: Float) {
|
|
||||||
_sliderProgress.value = value.toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAdapterItemChanged(value: Int) {
|
|
||||||
_adapterItemChanged.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDatasetChanged(value: Boolean) {
|
|
||||||
_datasetChanged.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setReloadListAndNotifyDataset(value: Boolean) {
|
|
||||||
_reloadListAndNotifyDataset.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldShowDeleteProfileDialog(profile: String) {
|
|
||||||
_shouldShowDeleteProfileDialog.value = profile
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldShowResetInputDialog(value: Boolean) {
|
|
||||||
_shouldShowResetInputDialog.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage =
|
|
||||||
try {
|
|
||||||
InputHandler.registeredControllers[currentDevice]
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
defaultParams
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.format.FormatStyle
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.DateTimeSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
|
|
||||||
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
|
||||||
SettingViewHolder(binding.root, adapter) {
|
|
||||||
private lateinit var setting: DateTimeSetting
|
|
||||||
|
|
||||||
override fun bind(item: SettingsItem) {
|
|
||||||
setting = item as DateTimeSetting
|
|
||||||
binding.textSettingName.text = item.title
|
|
||||||
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
|
|
||||||
binding.textSettingDescription.text = item.description
|
|
||||||
binding.textSettingValue.setVisible(true)
|
|
||||||
val epochTime = setting.getValue()
|
|
||||||
val instant = Instant.ofEpochMilli(epochTime * 1000)
|
|
||||||
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
|
|
||||||
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
|
|
||||||
binding.textSettingValue.text = dateFormatter.format(zonedTime)
|
|
||||||
|
|
||||||
binding.buttonClear.setVisible(setting.clearable)
|
|
||||||
binding.buttonClear.setOnClickListener {
|
|
||||||
adapter.onClearClick(setting, bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
setStyle(setting.isEditable, binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(clicked: View) {
|
|
||||||
if (setting.isEditable) {
|
|
||||||
adapter.onDateTimeClick(setting, bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(clicked: View): Boolean {
|
|
||||||
if (setting.isEditable) {
|
|
||||||
return adapter.onLongClick(setting, bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ListItemSettingsHeaderBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
|
|
||||||
|
|
||||||
class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: SettingsAdapter) :
|
|
||||||
SettingViewHolder(binding.root, adapter) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
itemView.setOnClickListener(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(item: SettingsItem) {
|
|
||||||
binding.textHeaderName.text = item.title
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(clicked: View) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(clicked: View): Boolean {
|
|
||||||
// no-op
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.InputProfileSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
|
|
||||||
class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
|
||||||
SettingViewHolder(binding.root, adapter) {
|
|
||||||
private lateinit var setting: InputProfileSetting
|
|
||||||
|
|
||||||
override fun bind(item: SettingsItem) {
|
|
||||||
setting = item as InputProfileSetting
|
|
||||||
binding.textSettingName.text = setting.title
|
|
||||||
binding.textSettingValue.text =
|
|
||||||
setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
|
|
||||||
|
|
||||||
binding.textSettingDescription.setVisible(false)
|
|
||||||
binding.buttonClear.setVisible(false)
|
|
||||||
binding.icon.setVisible(false)
|
|
||||||
binding.buttonClear.setVisible(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(clicked: View) =
|
|
||||||
adapter.onInputProfileClick(setting, bindingAdapterPosition)
|
|
||||||
|
|
||||||
override fun onLongClick(clicked: View): Boolean = false
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ListItemSettingBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SubmenuSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
|
|
||||||
class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
|
|
||||||
SettingViewHolder(binding.root, adapter) {
|
|
||||||
private lateinit var setting: SubmenuSetting
|
|
||||||
|
|
||||||
override fun bind(item: SettingsItem) {
|
|
||||||
setting = item as SubmenuSetting
|
|
||||||
binding.icon.setVisible(setting.iconId != 0)
|
|
||||||
if (setting.iconId != 0) {
|
|
||||||
binding.icon.setImageDrawable(
|
|
||||||
ResourcesCompat.getDrawable(
|
|
||||||
binding.icon.resources,
|
|
||||||
setting.iconId,
|
|
||||||
binding.icon.context.theme
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.textSettingName.text = setting.title
|
|
||||||
binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
|
|
||||||
binding.textSettingDescription.text = setting.description
|
|
||||||
binding.textSettingValue.setVisible(false)
|
|
||||||
binding.buttonClear.setVisible(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(clicked: View) {
|
|
||||||
adapter.onSubmenuClick(setting)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(clicked: View): Boolean {
|
|
||||||
// no-op
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.ui.viewholder
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ListItemSettingSwitchBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SettingsItem
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.view.SwitchSetting
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.ui.SettingsAdapter
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
|
|
||||||
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
|
|
||||||
SettingViewHolder(binding.root, adapter) {
|
|
||||||
|
|
||||||
private lateinit var setting: SwitchSetting
|
|
||||||
|
|
||||||
override fun bind(item: SettingsItem) {
|
|
||||||
setting = item as SwitchSetting
|
|
||||||
binding.textSettingName.text = setting.title
|
|
||||||
binding.textSettingDescription.setVisible(setting.description.isNotEmpty())
|
|
||||||
binding.textSettingDescription.text = setting.description
|
|
||||||
|
|
||||||
binding.switchWidget.setOnCheckedChangeListener(null)
|
|
||||||
binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
|
|
||||||
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
|
||||||
adapter.onBooleanClick(setting, binding.switchWidget.isChecked, bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonClear.setVisible(setting.clearable)
|
|
||||||
binding.buttonClear.setOnClickListener {
|
|
||||||
adapter.onClearClick(setting, bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
setStyle(setting.isEditable, binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(clicked: View) {
|
|
||||||
if (setting.isEditable) {
|
|
||||||
binding.switchWidget.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(clicked: View): Boolean {
|
|
||||||
if (setting.isEditable) {
|
|
||||||
return adapter.onLongClick(setting, bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.features.settings.utils
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import org.sudachi.sudachi_emu.model.Game
|
|
||||||
import java.io.*
|
|
||||||
import org.sudachi.sudachi_emu.utils.DirectoryInitialization
|
|
||||||
import org.sudachi.sudachi_emu.utils.FileUtil
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains static methods for interacting with .ini files in which settings are stored.
|
|
||||||
*/
|
|
||||||
object SettingsFile {
|
|
||||||
const val FILE_NAME_CONFIG = "config.ini"
|
|
||||||
|
|
||||||
fun getSettingsFile(fileName: String): File =
|
|
||||||
File(DirectoryInitialization.userDirectory + "/config/" + fileName)
|
|
||||||
|
|
||||||
fun getCustomSettingsFile(game: Game): File =
|
|
||||||
File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini")
|
|
||||||
|
|
||||||
fun loadCustomConfig(game: Game) {
|
|
||||||
val fileName = FileUtil.getFilename(Uri.parse(game.path))
|
|
||||||
NativeConfig.initializePerGameConfig(game.programId, fileName)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.DialogAddFolderBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.GameDir
|
|
||||||
import org.sudachi.sudachi_emu.model.GamesViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
|
|
||||||
class AddGameFolderDialogFragment : DialogFragment() {
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val binding = DialogAddFolderBinding.inflate(layoutInflater)
|
|
||||||
val folderUriString = requireArguments().getString(FOLDER_URI_STRING)
|
|
||||||
if (folderUriString == null) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
binding.path.text = Uri.parse(folderUriString).path
|
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.add_game_folder)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
|
||||||
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
|
|
||||||
homeViewModel.setGamesDirSelected(true)
|
|
||||||
gamesViewModel.addFolder(newGameDir)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setView(binding.root)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "AddGameFolderDialogFragment"
|
|
||||||
|
|
||||||
private const val FOLDER_URI_STRING = "FolderUriString"
|
|
||||||
|
|
||||||
fun newInstance(folderUriString: String): AddGameFolderDialogFragment {
|
|
||||||
val args = Bundle()
|
|
||||||
args.putString(FOLDER_URI_STRING, folderUriString)
|
|
||||||
val fragment = AddGameFolderDialogFragment()
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,205 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.adapters.AddonAdapter
|
|
||||||
import org.sudachi.sudachi_emu.databinding.FragmentAddonsBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.AddonViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.AddonUtil
|
|
||||||
import org.sudachi.sudachi_emu.utils.FileUtil.copyFilesTo
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.updateMargins
|
|
||||||
import org.sudachi.sudachi_emu.utils.collect
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class AddonsFragment : Fragment() {
|
|
||||||
private var _binding: FragmentAddonsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
|
||||||
private val addonViewModel: AddonViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private val args by navArgs<AddonsFragmentArgs>()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
addonViewModel.onOpenAddons(args.game)
|
|
||||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
|
||||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
|
||||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentAddonsBinding.inflate(inflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(false)
|
|
||||||
|
|
||||||
binding.toolbarAddons.setNavigationOnClickListener {
|
|
||||||
binding.root.findNavController().popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)
|
|
||||||
|
|
||||||
binding.listAddons.apply {
|
|
||||||
layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
adapter = AddonAdapter(addonViewModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
addonViewModel.addonList.collect(viewLifecycleOwner) {
|
|
||||||
(binding.listAddons.adapter as AddonAdapter).submitList(it)
|
|
||||||
}
|
|
||||||
addonViewModel.showModInstallPicker.collect(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
resetState = { addonViewModel.showModInstallPicker(false) }
|
|
||||||
) { if (it) installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }
|
|
||||||
addonViewModel.showModNoticeDialog.collect(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
resetState = { addonViewModel.showModNoticeDialog(false) }
|
|
||||||
) {
|
|
||||||
if (it) {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
|
||||||
titleId = R.string.addon_notice,
|
|
||||||
descriptionId = R.string.addon_notice_description,
|
|
||||||
dismissible = false,
|
|
||||||
positiveAction = { addonViewModel.showModInstallPicker(true) },
|
|
||||||
negativeAction = {},
|
|
||||||
negativeButtonTitleId = R.string.close
|
|
||||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addonViewModel.addonToDelete.collect(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
resetState = { addonViewModel.setAddonToDelete(null) }
|
|
||||||
) {
|
|
||||||
if (it != null) {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
|
||||||
titleId = R.string.confirm_uninstall,
|
|
||||||
descriptionId = R.string.confirm_uninstall_description,
|
|
||||||
positiveAction = { addonViewModel.onDeleteAddon(it) },
|
|
||||||
negativeAction = {}
|
|
||||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.buttonInstall.setOnClickListener {
|
|
||||||
ContentTypeSelectionDialogFragment().show(
|
|
||||||
parentFragmentManager,
|
|
||||||
ContentTypeSelectionDialogFragment.TAG
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
addonViewModel.refreshAddons()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
addonViewModel.onCloseAddons()
|
|
||||||
}
|
|
||||||
|
|
||||||
val installAddon =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result)
|
|
||||||
if (externalAddonDirectory == null) {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
|
||||||
titleId = R.string.invalid_directory,
|
|
||||||
descriptionId = R.string.invalid_directory_description
|
|
||||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val isValid = externalAddonDirectory.listFiles()
|
|
||||||
.any { AddonUtil.validAddonDirectories.contains(it.name?.lowercase()) }
|
|
||||||
val errorMessage = MessageDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
|
||||||
titleId = R.string.invalid_directory,
|
|
||||||
descriptionId = R.string.invalid_directory_description
|
|
||||||
)
|
|
||||||
if (isValid) {
|
|
||||||
ProgressDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
|
||||||
R.string.installing_game_content,
|
|
||||||
false
|
|
||||||
) { progressCallback, _ ->
|
|
||||||
val parentDirectoryName = externalAddonDirectory.name
|
|
||||||
val internalAddonDirectory =
|
|
||||||
File(args.game.addonDir + parentDirectoryName)
|
|
||||||
try {
|
|
||||||
externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@newInstance errorMessage
|
|
||||||
}
|
|
||||||
addonViewModel.refreshAddons()
|
|
||||||
return@newInstance getString(R.string.addon_installed_successfully)
|
|
||||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
|
||||||
} else {
|
|
||||||
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
|
|
||||||
val leftInsets = barInsets.left + cutoutInsets.left
|
|
||||||
val rightInsets = barInsets.right + cutoutInsets.right
|
|
||||||
|
|
||||||
binding.toolbarAddons.updateMargins(left = leftInsets, right = rightInsets)
|
|
||||||
binding.listAddons.updateMargins(left = leftInsets, right = rightInsets)
|
|
||||||
binding.listAddons.updatePadding(
|
|
||||||
bottom = barInsets.bottom +
|
|
||||||
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
|
||||||
)
|
|
||||||
|
|
||||||
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
|
||||||
binding.buttonInstall.updateMargins(
|
|
||||||
left = leftInsets + fabSpacing,
|
|
||||||
right = rightInsets + fabSpacing,
|
|
||||||
bottom = barInsets.bottom + fabSpacing
|
|
||||||
)
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.model.AddonViewModel
|
|
||||||
import org.sudachi.sudachi_emu.ui.main.MainActivity
|
|
||||||
|
|
||||||
class ContentTypeSelectionDialogFragment : DialogFragment() {
|
|
||||||
private val addonViewModel: AddonViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private val preferences get() =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(SudachiApplication.appContext)
|
|
||||||
|
|
||||||
private var selectedItem = 0
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val launchOptions =
|
|
||||||
arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats))
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
val mainActivity = requireActivity() as MainActivity
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.select_content_type)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
|
||||||
when (selectedItem) {
|
|
||||||
0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*"))
|
|
||||||
else -> {
|
|
||||||
if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) {
|
|
||||||
preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply()
|
|
||||||
addonViewModel.showModNoticeDialog(true)
|
|
||||||
return@setPositiveButton
|
|
||||||
}
|
|
||||||
addonViewModel.showModInstallPicker(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
|
|
||||||
selectedItem = i
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putInt(SELECTED_ITEM, selectedItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "ContentTypeSelectionDialogFragment"
|
|
||||||
|
|
||||||
private const val SELECTED_ITEM = "SelectedItem"
|
|
||||||
private const val MOD_NOTICE_SEEN = "ModNoticeSeen"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
|
|
||||||
class CoreErrorDialogFragment : DialogFragment() {
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
|
||||||
MaterialAlertDialogBuilder(requireActivity())
|
|
||||||
.setTitle(requireArguments().getString(TITLE))
|
|
||||||
.setMessage(requireArguments().getString(MESSAGE))
|
|
||||||
.setPositiveButton(R.string.continue_button, null)
|
|
||||||
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
|
|
||||||
NativeLibrary.coreErrorAlertResult = false
|
|
||||||
synchronized(NativeLibrary.coreErrorAlertLock) {
|
|
||||||
NativeLibrary.coreErrorAlertLock.notify()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.create()
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
NativeLibrary.coreErrorAlertResult = true
|
|
||||||
synchronized(NativeLibrary.coreErrorAlertLock) { NativeLibrary.coreErrorAlertLock.notify() }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TITLE = "Title"
|
|
||||||
const val MESSAGE = "Message"
|
|
||||||
|
|
||||||
fun newInstance(title: String, message: String): CoreErrorDialogFragment {
|
|
||||||
val frag = CoreErrorDialogFragment()
|
|
||||||
val args = Bundle()
|
|
||||||
args.putString(TITLE, title)
|
|
||||||
args.putString(MESSAGE, message)
|
|
||||||
frag.arguments = args
|
|
||||||
return frag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.DialogProgressBarBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.DriverViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.collect
|
|
||||||
|
|
||||||
class DriversLoadingDialogFragment : DialogFragment() {
|
|
||||||
private val driverViewModel: DriverViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private lateinit var binding: DialogProgressBarBinding
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
binding = DialogProgressBarBinding.inflate(layoutInflater)
|
|
||||||
binding.progressBar.isIndeterminate = true
|
|
||||||
|
|
||||||
isCancelable = false
|
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.loading)
|
|
||||||
.setView(binding.root)
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View = binding.root
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { if (it) dismiss() }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "DriversLoadingDialogFragment"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.FragmentEarlyAccessBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.updateMargins
|
|
||||||
|
|
||||||
class EarlyAccessFragment : Fragment() {
|
|
||||||
private var _binding: FragmentEarlyAccessBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
|
||||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentEarlyAccessBinding.inflate(layoutInflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
|
||||||
|
|
||||||
binding.toolbarAbout.setNavigationOnClickListener {
|
|
||||||
parentFragmentManager.primaryNavigationFragment?.findNavController()?.popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.getEarlyAccessButton.setOnClickListener {
|
|
||||||
openLink(
|
|
||||||
getString(R.string.play_store_link)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openLink(link: String) {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
|
|
||||||
val leftInsets = barInsets.left + cutoutInsets.left
|
|
||||||
val rightInsets = barInsets.right + cutoutInsets.right
|
|
||||||
|
|
||||||
binding.appbarEa.updateMargins(left = leftInsets, right = rightInsets)
|
|
||||||
|
|
||||||
binding.scrollEa.updatePadding(
|
|
||||||
left = leftInsets,
|
|
||||||
right = rightInsets,
|
|
||||||
bottom = barInsets.bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,78 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.DialogFolderPropertiesBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.GameDir
|
|
||||||
import org.sudachi.sudachi_emu.model.GamesViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
import org.sudachi.sudachi_emu.utils.SerializableHelper.parcelable
|
|
||||||
|
|
||||||
class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private var deepScan = false
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
|
||||||
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
|
||||||
|
|
||||||
// Restore checkbox state
|
|
||||||
binding.deepScanSwitch.isChecked =
|
|
||||||
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
|
||||||
|
|
||||||
// Ensure that we can get the checkbox state even if the view is destroyed
|
|
||||||
deepScan = binding.deepScanSwitch.isChecked
|
|
||||||
binding.deepScanSwitch.setOnClickListener {
|
|
||||||
deepScan = binding.deepScanSwitch.isChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setView(binding.root)
|
|
||||||
.setTitle(R.string.game_folder_properties)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
|
||||||
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
|
||||||
if (folderIndex != -1) {
|
|
||||||
gamesViewModel.folders.value[folderIndex].deepScan =
|
|
||||||
binding.deepScanSwitch.isChecked
|
|
||||||
gamesViewModel.updateGameDirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
NativeConfig.saveGlobalConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putBoolean(DEEP_SCAN, deepScan)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "GameFolderPropertiesDialogFragment"
|
|
||||||
|
|
||||||
private const val GAME_DIR = "GameDir"
|
|
||||||
|
|
||||||
private const val DEEP_SCAN = "DeepScan"
|
|
||||||
|
|
||||||
fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment {
|
|
||||||
val args = Bundle()
|
|
||||||
args.putParcelable(GAME_DIR, gameDir)
|
|
||||||
val fragment = GameFolderPropertiesDialogFragment()
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.FragmentGameInfoBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.GameVerificationResult
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.GameMetadata
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.updateMargins
|
|
||||||
|
|
||||||
class GameInfoFragment : Fragment() {
|
|
||||||
private var _binding: FragmentGameInfoBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private val args by navArgs<GameInfoFragmentArgs>()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
|
||||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
|
||||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
|
||||||
|
|
||||||
// Check for an up-to-date version string
|
|
||||||
args.game.version = GameMetadata.getVersion(args.game.path, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentGameInfoBinding.inflate(inflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(false)
|
|
||||||
|
|
||||||
binding.apply {
|
|
||||||
toolbarInfo.title = args.game.title
|
|
||||||
toolbarInfo.setNavigationOnClickListener {
|
|
||||||
view.findNavController().popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
val pathString = Uri.parse(args.game.path).path ?: ""
|
|
||||||
path.setHint(R.string.path)
|
|
||||||
pathField.setText(pathString)
|
|
||||||
pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) }
|
|
||||||
|
|
||||||
programId.setHint(R.string.program_id)
|
|
||||||
programIdField.setText(args.game.programIdHex)
|
|
||||||
programIdField.setOnClickListener {
|
|
||||||
copyToClipboard(getString(R.string.program_id), args.game.programIdHex)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.game.developer.isNotEmpty()) {
|
|
||||||
developer.setHint(R.string.developer)
|
|
||||||
developerField.setText(args.game.developer)
|
|
||||||
developerField.setOnClickListener {
|
|
||||||
copyToClipboard(getString(R.string.developer), args.game.developer)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
developer.setVisible(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
version.setHint(R.string.version)
|
|
||||||
versionField.setText(args.game.version)
|
|
||||||
versionField.setOnClickListener {
|
|
||||||
copyToClipboard(getString(R.string.version), args.game.version)
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonCopy.setOnClickListener {
|
|
||||||
val details = """
|
|
||||||
${args.game.title}
|
|
||||||
${getString(R.string.path)} - $pathString
|
|
||||||
${getString(R.string.program_id)} - ${args.game.programIdHex}
|
|
||||||
${getString(R.string.developer)} - ${args.game.developer}
|
|
||||||
${getString(R.string.version)} - ${args.game.version}
|
|
||||||
""".trimIndent()
|
|
||||||
copyToClipboard(args.game.title, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonVerifyIntegrity.setOnClickListener {
|
|
||||||
ProgressDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
|
||||||
R.string.verifying,
|
|
||||||
true
|
|
||||||
) { progressCallback, _ ->
|
|
||||||
val result = GameVerificationResult.from(
|
|
||||||
NativeLibrary.verifyGameContents(
|
|
||||||
args.game.path,
|
|
||||||
progressCallback
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return@newInstance when (result) {
|
|
||||||
GameVerificationResult.Success ->
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
titleId = R.string.verify_success,
|
|
||||||
descriptionId = R.string.operation_completed_successfully
|
|
||||||
)
|
|
||||||
|
|
||||||
GameVerificationResult.Failed ->
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
titleId = R.string.verify_failure,
|
|
||||||
descriptionId = R.string.verify_failure_description
|
|
||||||
)
|
|
||||||
|
|
||||||
GameVerificationResult.NotImplemented ->
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
titleId = R.string.verify_no_result,
|
|
||||||
descriptionId = R.string.verify_no_result_description
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyToClipboard(label: String, body: String) {
|
|
||||||
val clipBoard =
|
|
||||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText(label, body)
|
|
||||||
clipBoard.setPrimaryClip(clip)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
Toast.makeText(
|
|
||||||
requireContext(),
|
|
||||||
R.string.copied_to_clipboard,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
|
|
||||||
val leftInsets = barInsets.left + cutoutInsets.left
|
|
||||||
val rightInsets = barInsets.right + cutoutInsets.right
|
|
||||||
|
|
||||||
binding.toolbarInfo.updateMargins(left = leftInsets, right = rightInsets)
|
|
||||||
binding.scrollInfo.updateMargins(left = leftInsets, right = rightInsets)
|
|
||||||
|
|
||||||
binding.contentInfo.updatePadding(bottom = barInsets.bottom)
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
import org.sudachi.sudachi_emu.databinding.DialogLicenseBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.License
|
|
||||||
import org.sudachi.sudachi_emu.utils.SerializableHelper.parcelable
|
|
||||||
|
|
||||||
class LicenseBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|
||||||
private var _binding: DialogLicenseBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = DialogLicenseBinding.inflate(layoutInflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
BottomSheetBehavior.from<View>(view.parent as View).state =
|
|
||||||
BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
||||||
|
|
||||||
val license = requireArguments().parcelable<License>(LICENSE)!!
|
|
||||||
|
|
||||||
binding.apply {
|
|
||||||
textTitle.setText(license.titleId)
|
|
||||||
textLink.setText(license.linkId)
|
|
||||||
textCopyright.setText(license.copyrightId)
|
|
||||||
textLicense.setText(license.licenseId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "LicenseBottomSheetDialogFragment"
|
|
||||||
|
|
||||||
const val LICENSE = "License"
|
|
||||||
|
|
||||||
fun newInstance(
|
|
||||||
license: License
|
|
||||||
): LicenseBottomSheetDialogFragment {
|
|
||||||
val dialog = LicenseBottomSheetDialogFragment()
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putParcelable(LICENSE, license)
|
|
||||||
dialog.arguments = bundle
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.adapters.LicenseAdapter
|
|
||||||
import org.sudachi.sudachi_emu.databinding.FragmentLicensesBinding
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.License
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.updateMargins
|
|
||||||
|
|
||||||
class LicensesFragment : Fragment() {
|
|
||||||
private var _binding: FragmentLicensesBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
|
||||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentLicensesBinding.inflate(layoutInflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
|
||||||
|
|
||||||
binding.toolbarLicenses.setNavigationOnClickListener {
|
|
||||||
binding.root.findNavController().popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
val licenses = listOf(
|
|
||||||
License(
|
|
||||||
R.string.license_fidelityfx_fsr,
|
|
||||||
R.string.license_fidelityfx_fsr_description,
|
|
||||||
R.string.license_fidelityfx_fsr_link,
|
|
||||||
R.string.license_fidelityfx_fsr_copyright,
|
|
||||||
R.string.license_fidelityfx_fsr_text
|
|
||||||
),
|
|
||||||
License(
|
|
||||||
R.string.license_cubeb,
|
|
||||||
R.string.license_cubeb_description,
|
|
||||||
R.string.license_cubeb_link,
|
|
||||||
R.string.license_cubeb_copyright,
|
|
||||||
R.string.license_cubeb_text
|
|
||||||
),
|
|
||||||
License(
|
|
||||||
R.string.license_dynarmic,
|
|
||||||
R.string.license_dynarmic_description,
|
|
||||||
R.string.license_dynarmic_link,
|
|
||||||
R.string.license_dynarmic_copyright,
|
|
||||||
R.string.license_dynarmic_text
|
|
||||||
),
|
|
||||||
License(
|
|
||||||
R.string.license_ffmpeg,
|
|
||||||
R.string.license_ffmpeg_description,
|
|
||||||
R.string.license_ffmpeg_link,
|
|
||||||
R.string.license_ffmpeg_copyright,
|
|
||||||
R.string.license_ffmpeg_text
|
|
||||||
),
|
|
||||||
License(
|
|
||||||
R.string.license_opus,
|
|
||||||
R.string.license_opus_description,
|
|
||||||
R.string.license_opus_link,
|
|
||||||
R.string.license_opus_copyright,
|
|
||||||
R.string.license_opus_text
|
|
||||||
),
|
|
||||||
License(
|
|
||||||
R.string.license_sirit,
|
|
||||||
R.string.license_sirit_description,
|
|
||||||
R.string.license_sirit_link,
|
|
||||||
R.string.license_sirit_copyright,
|
|
||||||
R.string.license_sirit_text
|
|
||||||
),
|
|
||||||
License(
|
|
||||||
R.string.license_adreno_tools,
|
|
||||||
R.string.license_adreno_tools_description,
|
|
||||||
R.string.license_adreno_tools_link,
|
|
||||||
R.string.license_adreno_tools_copyright,
|
|
||||||
R.string.license_adreno_tools_text
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.listLicenses.apply {
|
|
||||||
layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
adapter = LicenseAdapter(requireActivity() as AppCompatActivity, licenses)
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
|
|
||||||
val leftInsets = barInsets.left + cutoutInsets.left
|
|
||||||
val rightInsets = barInsets.right + cutoutInsets.right
|
|
||||||
|
|
||||||
binding.appbarLicenses.updateMargins(left = leftInsets, right = rightInsets)
|
|
||||||
binding.listLicenses.updateMargins(left = leftInsets, right = rightInsets)
|
|
||||||
|
|
||||||
binding.listLicenses.updatePadding(bottom = barInsets.bottom)
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
|
|
||||||
class PermissionDeniedDialogFragment : DialogFragment() {
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setPositiveButton(R.string.home_settings) { _: DialogInterface?, _: Int ->
|
|
||||||
openSettings()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setTitle(R.string.permission_denied)
|
|
||||||
.setMessage(R.string.permission_denied_description)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openSettings() {
|
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
|
||||||
val uri = Uri.fromParts("package", requireActivity().packageName, null)
|
|
||||||
intent.data = uri
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "PermissionDeniedDialogFragment"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.ui.SettingsActivity
|
|
||||||
|
|
||||||
class ResetSettingsDialogFragment : DialogFragment() {
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val settingsActivity = requireActivity() as SettingsActivity
|
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setTitle(R.string.reset_all_settings)
|
|
||||||
.setMessage(R.string.reset_all_settings_description)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
settingsActivity.onSettingsReset()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "ResetSettingsDialogFragment"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,218 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.fragments
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.core.widget.doOnTextChanged
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import info.debatty.java.stringsimilarity.Jaccard
|
|
||||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
|
||||||
import java.util.Locale
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.adapters.GameAdapter
|
|
||||||
import org.sudachi.sudachi_emu.databinding.FragmentSearchBinding
|
|
||||||
import org.sudachi.sudachi_emu.layout.AutofitGridLayoutManager
|
|
||||||
import org.sudachi.sudachi_emu.model.Game
|
|
||||||
import org.sudachi.sudachi_emu.model.GamesViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
import org.sudachi.sudachi_emu.utils.collect
|
|
||||||
|
|
||||||
class SearchFragment : Fragment() {
|
|
||||||
private var _binding: FragmentSearchBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private lateinit var preferences: SharedPreferences
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SEARCH_TEXT = "SearchText"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentSearchBinding.inflate(layoutInflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(true)
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(SudachiApplication.appContext)
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.gridGamesSearch.apply {
|
|
||||||
layoutManager = AutofitGridLayoutManager(
|
|
||||||
requireContext(),
|
|
||||||
requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
|
|
||||||
)
|
|
||||||
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
|
|
||||||
|
|
||||||
binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
|
|
||||||
binding.clearButton.setVisible(text.toString().isNotEmpty())
|
|
||||||
filterAndSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
gamesViewModel.searchFocused.collect(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
resetState = { gamesViewModel.setSearchFocused(false) }
|
|
||||||
) { if (it) focusSearch() }
|
|
||||||
gamesViewModel.games.collect(viewLifecycleOwner) { filterAndSearch() }
|
|
||||||
gamesViewModel.searchedGames.collect(viewLifecycleOwner) {
|
|
||||||
(binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
|
|
||||||
binding.noResultsView.setVisible(it.isNotEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
|
|
||||||
|
|
||||||
binding.searchBackground.setOnClickListener { focusSearch() }
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
filterAndSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ScoredGame(val score: Double, val item: Game)
|
|
||||||
|
|
||||||
private fun filterAndSearch() {
|
|
||||||
val baseList = gamesViewModel.games.value
|
|
||||||
val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) {
|
|
||||||
R.id.chip_recently_played -> {
|
|
||||||
baseList.filter {
|
|
||||||
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
|
|
||||||
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
|
||||||
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.chip_recently_added -> {
|
|
||||||
baseList.filter {
|
|
||||||
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
|
|
||||||
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
|
||||||
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.chip_homebrew -> baseList.filter { it.isHomebrew }
|
|
||||||
|
|
||||||
R.id.chip_retail -> baseList.filter { !it.isHomebrew }
|
|
||||||
|
|
||||||
else -> baseList
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.searchText.text.toString().isEmpty() &&
|
|
||||||
binding.chipGroup.checkedChipId != View.NO_ID
|
|
||||||
) {
|
|
||||||
gamesViewModel.setSearchedGames(filteredList)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
|
||||||
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
|
|
||||||
val sortedList: List<Game> = filteredList.mapNotNull { game ->
|
|
||||||
val title = game.title.lowercase(Locale.getDefault())
|
|
||||||
val score = searchAlgorithm.similarity(searchTerm, title)
|
|
||||||
if (score > 0.03) {
|
|
||||||
ScoredGame(score, game)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.sortedByDescending { it.score }.map { it.item }
|
|
||||||
gamesViewModel.setSearchedGames(sortedList)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
if (_binding != null) {
|
|
||||||
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun focusSearch() {
|
|
||||||
if (_binding != null) {
|
|
||||||
binding.searchText.requestFocus()
|
|
||||||
val imm = requireActivity()
|
|
||||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
|
|
||||||
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { view: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
|
|
||||||
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
|
||||||
val spacingNavigationRail =
|
|
||||||
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
|
|
||||||
val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
|
|
||||||
|
|
||||||
binding.constraintSearch.updatePadding(
|
|
||||||
left = barInsets.left + cutoutInsets.left,
|
|
||||||
top = barInsets.top,
|
|
||||||
right = barInsets.right + cutoutInsets.right
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.gridGamesSearch.updatePadding(
|
|
||||||
top = extraListSpacing,
|
|
||||||
bottom = barInsets.bottom + spacingNavigation + extraListSpacing
|
|
||||||
)
|
|
||||||
binding.noResultsView.updatePadding(bottom = spacingNavigation + barInsets.bottom)
|
|
||||||
|
|
||||||
val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
|
||||||
binding.frameSearch.updatePadding(left = spacingNavigationRail)
|
|
||||||
binding.gridGamesSearch.updatePadding(left = spacingNavigationRail)
|
|
||||||
binding.noResultsView.updatePadding(left = spacingNavigationRail)
|
|
||||||
binding.chipGroup.updatePadding(
|
|
||||||
left = chipSpacing + spacingNavigationRail,
|
|
||||||
right = chipSpacing
|
|
||||||
)
|
|
||||||
mlpDivider.leftMargin = chipSpacing + spacingNavigationRail
|
|
||||||
mlpDivider.rightMargin = chipSpacing
|
|
||||||
} else {
|
|
||||||
binding.frameSearch.updatePadding(right = spacingNavigationRail)
|
|
||||||
binding.gridGamesSearch.updatePadding(right = spacingNavigationRail)
|
|
||||||
binding.noResultsView.updatePadding(right = spacingNavigationRail)
|
|
||||||
binding.chipGroup.updatePadding(
|
|
||||||
left = chipSpacing,
|
|
||||||
right = chipSpacing + spacingNavigationRail
|
|
||||||
)
|
|
||||||
mlpDivider.leftMargin = chipSpacing
|
|
||||||
mlpDivider.rightMargin = chipSpacing + spacingNavigationRail
|
|
||||||
}
|
|
||||||
binding.divider.layoutParams = mlpDivider
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.layout
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.Recycler
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cut down version of the solution provided here
|
|
||||||
* https://stackoverflow.com/questions/26666143/recyclerview-gridlayoutmanager-how-to-auto-detect-span-count
|
|
||||||
*/
|
|
||||||
class AutofitGridLayoutManager(
|
|
||||||
context: Context,
|
|
||||||
columnWidth: Int
|
|
||||||
) : GridLayoutManager(context, 1) {
|
|
||||||
private var columnWidth = 0
|
|
||||||
private var isColumnWidthChanged = true
|
|
||||||
private var lastWidth = 0
|
|
||||||
private var lastHeight = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
setColumnWidth(checkedColumnWidth(context, columnWidth))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkedColumnWidth(context: Context, columnWidth: Int): Int {
|
|
||||||
var newColumnWidth = columnWidth
|
|
||||||
if (newColumnWidth <= 0) {
|
|
||||||
newColumnWidth = context.resources.getDimensionPixelSize(R.dimen.spacing_xtralarge)
|
|
||||||
}
|
|
||||||
return newColumnWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setColumnWidth(newColumnWidth: Int) {
|
|
||||||
if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
|
|
||||||
columnWidth = newColumnWidth
|
|
||||||
isColumnWidthChanged = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {
|
|
||||||
val width = width
|
|
||||||
val height = height
|
|
||||||
if (columnWidth > 0 && width > 0 && height > 0 &&
|
|
||||||
(isColumnWidthChanged || lastWidth != width || lastHeight != height)
|
|
||||||
) {
|
|
||||||
val totalSpace: Int = if (orientation == VERTICAL) {
|
|
||||||
width - paddingRight - paddingLeft
|
|
||||||
} else {
|
|
||||||
height - paddingTop - paddingBottom
|
|
||||||
}
|
|
||||||
val spanCount = 1.coerceAtLeast(totalSpace / columnWidth)
|
|
||||||
setSpanCount(spanCount)
|
|
||||||
isColumnWidthChanged = false
|
|
||||||
}
|
|
||||||
lastWidth = width
|
|
||||||
lastHeight = height
|
|
||||||
super.onLayoutChildren(recycler, state)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
class AddonViewModel : ViewModel() {
|
|
||||||
private val _patchList = MutableStateFlow(mutableListOf<Patch>())
|
|
||||||
val addonList get() = _patchList.asStateFlow()
|
|
||||||
|
|
||||||
private val _showModInstallPicker = MutableStateFlow(false)
|
|
||||||
val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
|
|
||||||
|
|
||||||
private val _showModNoticeDialog = MutableStateFlow(false)
|
|
||||||
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
|
|
||||||
|
|
||||||
private val _addonToDelete = MutableStateFlow<Patch?>(null)
|
|
||||||
val addonToDelete = _addonToDelete.asStateFlow()
|
|
||||||
|
|
||||||
var game: Game? = null
|
|
||||||
|
|
||||||
private val isRefreshing = AtomicBoolean(false)
|
|
||||||
|
|
||||||
fun onOpenAddons(game: Game) {
|
|
||||||
this.game = game
|
|
||||||
refreshAddons()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshAddons() {
|
|
||||||
if (isRefreshing.get() || game == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isRefreshing.set(true)
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val patchList = (
|
|
||||||
NativeLibrary.getPatchesForFile(game!!.path, game!!.programId)
|
|
||||||
?: emptyArray()
|
|
||||||
).toMutableList()
|
|
||||||
patchList.sortBy { it.name }
|
|
||||||
_patchList.value = patchList
|
|
||||||
isRefreshing.set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAddonToDelete(patch: Patch?) {
|
|
||||||
_addonToDelete.value = patch
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDeleteAddon(patch: Patch) {
|
|
||||||
when (PatchType.from(patch.type)) {
|
|
||||||
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
|
|
||||||
PatchType.DLC -> NativeLibrary.removeDLC(patch.programId)
|
|
||||||
PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name)
|
|
||||||
}
|
|
||||||
refreshAddons()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCloseAddons() {
|
|
||||||
if (_patchList.value.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeConfig.setDisabledAddons(
|
|
||||||
game!!.programId,
|
|
||||||
_patchList.value.mapNotNull {
|
|
||||||
if (it.enabled) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
it.name
|
|
||||||
}
|
|
||||||
}.toTypedArray()
|
|
||||||
)
|
|
||||||
NativeConfig.saveGlobalConfig()
|
|
||||||
_patchList.value.clear()
|
|
||||||
game = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showModInstallPicker(install: Boolean) {
|
|
||||||
_showModInstallPicker.value = install
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showModNoticeDialog(show: Boolean) {
|
|
||||||
_showModNoticeDialog.value = show
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
|
|
||||||
data class Applet(
|
|
||||||
@StringRes val titleId: Int,
|
|
||||||
@StringRes val descriptionId: Int,
|
|
||||||
@DrawableRes val iconId: Int,
|
|
||||||
val appletInfo: AppletInfo,
|
|
||||||
val cabinetMode: CabinetMode = CabinetMode.None
|
|
||||||
)
|
|
||||||
|
|
||||||
// Combination of Common::AM::Applets::AppletId enum and the entry id
|
|
||||||
enum class AppletInfo(val appletId: Int, val entryId: Long = 0) {
|
|
||||||
None(0x00),
|
|
||||||
Application(0x01),
|
|
||||||
OverlayDisplay(0x02),
|
|
||||||
QLaunch(0x03),
|
|
||||||
Starter(0x04),
|
|
||||||
Auth(0x0A),
|
|
||||||
Cabinet(0x0B, 0x0100000000001002),
|
|
||||||
Controller(0x0C),
|
|
||||||
DataErase(0x0D),
|
|
||||||
Error(0x0E),
|
|
||||||
NetConnect(0x0F),
|
|
||||||
ProfileSelect(0x10),
|
|
||||||
SoftwareKeyboard(0x11),
|
|
||||||
MiiEdit(0x12, 0x0100000000001009),
|
|
||||||
Web(0x13),
|
|
||||||
Shop(0x14),
|
|
||||||
PhotoViewer(0x015, 0x010000000000100D),
|
|
||||||
Settings(0x16),
|
|
||||||
OfflineWeb(0x17),
|
|
||||||
LoginShare(0x18),
|
|
||||||
WebAuth(0x19),
|
|
||||||
MyPage(0x1A)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches enum in Service::NFP::CabinetMode with extra metadata
|
|
||||||
enum class CabinetMode(
|
|
||||||
val id: Int,
|
|
||||||
@StringRes val titleId: Int = 0,
|
|
||||||
@DrawableRes val iconId: Int = 0
|
|
||||||
) {
|
|
||||||
None(-1),
|
|
||||||
StartNicknameAndOwnerSettings(0, R.string.cabinet_nickname_and_owner, R.drawable.ic_edit),
|
|
||||||
StartGameDataEraser(1, R.string.cabinet_game_data_eraser, R.drawable.ic_refresh),
|
|
||||||
StartRestorer(2, R.string.cabinet_restorer, R.drawable.ic_restore),
|
|
||||||
StartFormatter(3, R.string.cabinet_formatter, R.drawable.ic_clear)
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.utils.GpuDriverMetadata
|
|
||||||
|
|
||||||
data class Driver(
|
|
||||||
override var selected: Boolean,
|
|
||||||
val title: String,
|
|
||||||
val version: String = "",
|
|
||||||
val description: String = ""
|
|
||||||
) : SelectableItem {
|
|
||||||
override fun onSelectionStateChanged(selected: Boolean) {
|
|
||||||
this.selected = selected
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun GpuDriverMetadata.toDriver(selected: Boolean = false): Driver =
|
|
||||||
Driver(
|
|
||||||
selected,
|
|
||||||
this.name ?: "",
|
|
||||||
this.version ?: "",
|
|
||||||
this.description ?: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.utils.GameHelper
|
|
||||||
import org.sudachi.sudachi_emu.utils.NativeConfig
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
class GamesViewModel : ViewModel() {
|
|
||||||
val games: StateFlow<List<Game>> get() = _games
|
|
||||||
private val _games = MutableStateFlow(emptyList<Game>())
|
|
||||||
|
|
||||||
val searchedGames: StateFlow<List<Game>> get() = _searchedGames
|
|
||||||
private val _searchedGames = MutableStateFlow(emptyList<Game>())
|
|
||||||
|
|
||||||
val isReloading: StateFlow<Boolean> get() = _isReloading
|
|
||||||
private val _isReloading = MutableStateFlow(false)
|
|
||||||
|
|
||||||
private val reloading = AtomicBoolean(false)
|
|
||||||
|
|
||||||
val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
|
|
||||||
private val _shouldSwapData = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val shouldScrollToTop: StateFlow<Boolean> get() = _shouldScrollToTop
|
|
||||||
private val _shouldScrollToTop = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
|
||||||
private val _searchFocused = MutableStateFlow(false)
|
|
||||||
|
|
||||||
private val _folders = MutableStateFlow(mutableListOf<GameDir>())
|
|
||||||
val folders = _folders.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
|
||||||
NativeLibrary.reloadKeys()
|
|
||||||
|
|
||||||
getGameDirs()
|
|
||||||
reloadGames(directoriesChanged = false, firstStartup = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGames(games: List<Game>) {
|
|
||||||
val sortedList = games.sortedWith(
|
|
||||||
compareBy(
|
|
||||||
{ it.title.lowercase(Locale.getDefault()) },
|
|
||||||
{ it.path }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
_games.value = sortedList
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchedGames(games: List<Game>) {
|
|
||||||
_searchedGames.value = games
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldSwapData(shouldSwap: Boolean) {
|
|
||||||
_shouldSwapData.value = shouldSwap
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldScrollToTop(shouldScroll: Boolean) {
|
|
||||||
_shouldScrollToTop.value = shouldScroll
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchFocused(searchFocused: Boolean) {
|
|
||||||
_searchFocused.value = searchFocused
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
|
|
||||||
if (reloading.get()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
reloading.set(true)
|
|
||||||
_isReloading.value = true
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
if (firstStartup) {
|
|
||||||
// Retrieve list of cached games
|
|
||||||
val storedGames =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(SudachiApplication.appContext)
|
|
||||||
.getStringSet(GameHelper.KEY_GAMES, emptySet())
|
|
||||||
if (storedGames!!.isNotEmpty()) {
|
|
||||||
val deserializedGames = mutableSetOf<Game>()
|
|
||||||
storedGames.forEach {
|
|
||||||
val game: Game
|
|
||||||
try {
|
|
||||||
game = Json.decodeFromString(it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// We don't care about any errors related to parsing the game cache
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val gameExists =
|
|
||||||
DocumentFile.fromSingleUri(
|
|
||||||
SudachiApplication.appContext,
|
|
||||||
Uri.parse(game.path)
|
|
||||||
)?.exists()
|
|
||||||
if (gameExists == true) {
|
|
||||||
deserializedGames.add(game)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setGames(deserializedGames.toList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setGames(GameHelper.getGames())
|
|
||||||
reloading.set(false)
|
|
||||||
_isReloading.value = false
|
|
||||||
|
|
||||||
if (directoriesChanged) {
|
|
||||||
setShouldSwapData(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addFolder(gameDir: GameDir) =
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
NativeConfig.addGameDir(gameDir)
|
|
||||||
getGameDirs(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeFolder(gameDir: GameDir) =
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val gameDirs = _folders.value.toMutableList()
|
|
||||||
val removedDirIndex = gameDirs.indexOf(gameDir)
|
|
||||||
if (removedDirIndex != -1) {
|
|
||||||
gameDirs.removeAt(removedDirIndex)
|
|
||||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
|
||||||
getGameDirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateGameDirs() =
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
NativeConfig.setGameDirs(_folders.value.toTypedArray())
|
|
||||||
getGameDirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onOpenGameFoldersFragment() =
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
getGameDirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onCloseGameFoldersFragment() {
|
|
||||||
NativeConfig.saveGlobalConfig()
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
getGameDirs(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGameDirs(reloadList: Boolean = false) {
|
|
||||||
val gameDirs = NativeConfig.getGameDirs()
|
|
||||||
_folders.value = gameDirs.toMutableList()
|
|
||||||
if (reloadList) {
|
|
||||||
reloadGames(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
data class HomeSetting(
|
|
||||||
val titleId: Int,
|
|
||||||
val descriptionId: Int,
|
|
||||||
val iconId: Int,
|
|
||||||
val onClick: () -> Unit,
|
|
||||||
val isEnabled: () -> Boolean = { true },
|
|
||||||
val disabledTitleId: Int = 0,
|
|
||||||
val disabledMessageId: Int = 0,
|
|
||||||
val details: StateFlow<String> = MutableStateFlow("")
|
|
||||||
)
|
|
|
@ -1,76 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
|
||||||
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
|
||||||
private val _navigationVisible = MutableStateFlow(Pair(false, false))
|
|
||||||
|
|
||||||
val statusBarShadeVisible: StateFlow<Boolean> get() = _statusBarShadeVisible
|
|
||||||
private val _statusBarShadeVisible = MutableStateFlow(true)
|
|
||||||
|
|
||||||
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
|
||||||
private val _shouldPageForward = MutableStateFlow(false)
|
|
||||||
|
|
||||||
private val _gamesDirSelected = MutableStateFlow(false)
|
|
||||||
val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
|
|
||||||
|
|
||||||
private val _openImportSaves = MutableStateFlow(false)
|
|
||||||
val openImportSaves get() = _openImportSaves.asStateFlow()
|
|
||||||
|
|
||||||
private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
|
|
||||||
val contentToInstall get() = _contentToInstall.asStateFlow()
|
|
||||||
|
|
||||||
private val _reloadPropertiesList = MutableStateFlow(false)
|
|
||||||
val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
|
|
||||||
|
|
||||||
private val _checkKeys = MutableStateFlow(false)
|
|
||||||
val checkKeys = _checkKeys.asStateFlow()
|
|
||||||
|
|
||||||
var navigatedToSetup = false
|
|
||||||
|
|
||||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
|
||||||
if (navigationVisible.value.first == visible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_navigationVisible.value = Pair(visible, animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStatusBarShadeVisibility(visible: Boolean) {
|
|
||||||
if (statusBarShadeVisible.value == visible) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_statusBarShadeVisible.value = visible
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShouldPageForward(pageForward: Boolean) {
|
|
||||||
_shouldPageForward.value = pageForward
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setGamesDirSelected(selected: Boolean) {
|
|
||||||
_gamesDirSelected.value = selected
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOpenImportSaves(import: Boolean) {
|
|
||||||
_openImportSaves.value = import
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setContentToInstall(documents: List<Uri>?) {
|
|
||||||
_contentToInstall.value = documents
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reloadPropertiesList(reload: Boolean) {
|
|
||||||
_reloadPropertiesList.value = reload
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCheckKeys(value: Boolean) {
|
|
||||||
_checkKeys.value = value
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
enum class InstallResult(val int: Int) {
|
|
||||||
Success(0),
|
|
||||||
Overwrite(1),
|
|
||||||
Failure(2),
|
|
||||||
BaseInstallAttempted(3);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class License(
|
|
||||||
val titleId: Int,
|
|
||||||
val descriptionId: Int,
|
|
||||||
val linkId: Int,
|
|
||||||
val copyrightId: Int,
|
|
||||||
val licenseId: Int
|
|
||||||
) : Parcelable
|
|
|
@ -1,16 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
data class Patch(
|
|
||||||
var enabled: Boolean,
|
|
||||||
val name: String,
|
|
||||||
val version: String,
|
|
||||||
val type: Int,
|
|
||||||
val programId: String,
|
|
||||||
val titleId: String
|
|
||||||
)
|
|
|
@ -1,14 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
enum class PatchType(val int: Int) {
|
|
||||||
Update(0),
|
|
||||||
DLC(1),
|
|
||||||
Mod(2);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.model
|
|
||||||
|
|
||||||
interface SelectableItem {
|
|
||||||
var selected: Boolean
|
|
||||||
fun onSelectionStateChanged(selected: Boolean)
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.overlay
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput.ButtonState
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeButton
|
|
||||||
import org.sudachi.sudachi_emu.overlay.model.OverlayControlData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom [BitmapDrawable] that is capable
|
|
||||||
* of storing it's own ID.
|
|
||||||
*
|
|
||||||
* @param res [Resources] instance.
|
|
||||||
* @param defaultStateBitmap [Bitmap] to use with the default state Drawable.
|
|
||||||
* @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable.
|
|
||||||
* @param button [NativeButton] for this type of button.
|
|
||||||
*/
|
|
||||||
class InputOverlayDrawableButton(
|
|
||||||
res: Resources,
|
|
||||||
defaultStateBitmap: Bitmap,
|
|
||||||
pressedStateBitmap: Bitmap,
|
|
||||||
val button: NativeButton,
|
|
||||||
val overlayControlData: OverlayControlData
|
|
||||||
) {
|
|
||||||
// The ID value what motion event is tracking
|
|
||||||
var trackId: Int
|
|
||||||
|
|
||||||
// The drawable position on the screen
|
|
||||||
private var buttonPositionX = 0
|
|
||||||
private var buttonPositionY = 0
|
|
||||||
|
|
||||||
val width: Int
|
|
||||||
val height: Int
|
|
||||||
|
|
||||||
private val defaultStateBitmap: BitmapDrawable
|
|
||||||
private val pressedStateBitmap: BitmapDrawable
|
|
||||||
private var pressedState = false
|
|
||||||
|
|
||||||
private var previousTouchX = 0
|
|
||||||
private var previousTouchY = 0
|
|
||||||
var controlPositionX = 0
|
|
||||||
var controlPositionY = 0
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
|
|
||||||
this.pressedStateBitmap = BitmapDrawable(res, pressedStateBitmap)
|
|
||||||
trackId = -1
|
|
||||||
width = this.defaultStateBitmap.intrinsicWidth
|
|
||||||
height = this.defaultStateBitmap.intrinsicHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates button status based on the motion event.
|
|
||||||
*
|
|
||||||
* @return true if value was changed
|
|
||||||
*/
|
|
||||||
fun updateStatus(event: MotionEvent): Boolean {
|
|
||||||
val pointerIndex = event.actionIndex
|
|
||||||
val xPosition = event.getX(pointerIndex).toInt()
|
|
||||||
val yPosition = event.getY(pointerIndex).toInt()
|
|
||||||
val pointerId = event.getPointerId(pointerIndex)
|
|
||||||
val motionEvent = event.action and MotionEvent.ACTION_MASK
|
|
||||||
val isActionDown =
|
|
||||||
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
|
|
||||||
val isActionUp =
|
|
||||||
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
|
|
||||||
|
|
||||||
if (isActionDown) {
|
|
||||||
if (!bounds.contains(xPosition, yPosition)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
pressedState = true
|
|
||||||
trackId = pointerId
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActionUp) {
|
|
||||||
if (trackId != pointerId) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
pressedState = false
|
|
||||||
trackId = -1
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPosition(x: Int, y: Int) {
|
|
||||||
buttonPositionX = x
|
|
||||||
buttonPositionY = y
|
|
||||||
}
|
|
||||||
|
|
||||||
fun draw(canvas: Canvas?) {
|
|
||||||
currentStateBitmapDrawable.draw(canvas!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val currentStateBitmapDrawable: BitmapDrawable
|
|
||||||
get() = if (pressedState) pressedStateBitmap else defaultStateBitmap
|
|
||||||
|
|
||||||
fun onConfigureTouch(event: MotionEvent): Boolean {
|
|
||||||
val pointerIndex = event.actionIndex
|
|
||||||
val fingerPositionX = event.getX(pointerIndex).toInt()
|
|
||||||
val fingerPositionY = event.getY(pointerIndex).toInt()
|
|
||||||
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
previousTouchX = fingerPositionX
|
|
||||||
previousTouchY = fingerPositionY
|
|
||||||
controlPositionX = fingerPositionX - (width / 2)
|
|
||||||
controlPositionY = fingerPositionY - (height / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
controlPositionX += fingerPositionX - previousTouchX
|
|
||||||
controlPositionY += fingerPositionY - previousTouchY
|
|
||||||
setBounds(
|
|
||||||
controlPositionX,
|
|
||||||
controlPositionY,
|
|
||||||
width + controlPositionX,
|
|
||||||
height + controlPositionY
|
|
||||||
)
|
|
||||||
previousTouchX = fingerPositionX
|
|
||||||
previousTouchY = fingerPositionY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
defaultStateBitmap.setBounds(left, top, right, bottom)
|
|
||||||
pressedStateBitmap.setBounds(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOpacity(value: Int) {
|
|
||||||
defaultStateBitmap.alpha = value
|
|
||||||
pressedStateBitmap.alpha = value
|
|
||||||
}
|
|
||||||
|
|
||||||
val status: Int
|
|
||||||
get() = if (pressedState) ButtonState.PRESSED else ButtonState.RELEASED
|
|
||||||
val bounds: Rect
|
|
||||||
get() = defaultStateBitmap.bounds
|
|
||||||
}
|
|
|
@ -1,266 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.overlay
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.BitmapDrawable
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput.ButtonState
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.NativeButton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom [BitmapDrawable] that is capable
|
|
||||||
* of storing it's own ID.
|
|
||||||
*
|
|
||||||
* @param res [Resources] instance.
|
|
||||||
* @param defaultStateBitmap [Bitmap] of the default state.
|
|
||||||
* @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction.
|
|
||||||
* @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction.
|
|
||||||
*/
|
|
||||||
class InputOverlayDrawableDpad(
|
|
||||||
res: Resources,
|
|
||||||
defaultStateBitmap: Bitmap,
|
|
||||||
pressedOneDirectionStateBitmap: Bitmap,
|
|
||||||
pressedTwoDirectionsStateBitmap: Bitmap
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Gets one of the InputOverlayDrawableDpad's button IDs.
|
|
||||||
*
|
|
||||||
* @return the requested InputOverlayDrawableDpad's button ID.
|
|
||||||
*/
|
|
||||||
// The ID identifying what type of button this Drawable represents.
|
|
||||||
val up = NativeButton.DUp
|
|
||||||
val down = NativeButton.DDown
|
|
||||||
val left = NativeButton.DLeft
|
|
||||||
val right = NativeButton.DRight
|
|
||||||
var trackId: Int
|
|
||||||
|
|
||||||
val width: Int
|
|
||||||
val height: Int
|
|
||||||
|
|
||||||
private val defaultStateBitmap: BitmapDrawable
|
|
||||||
private val pressedOneDirectionStateBitmap: BitmapDrawable
|
|
||||||
private val pressedTwoDirectionsStateBitmap: BitmapDrawable
|
|
||||||
|
|
||||||
private var previousTouchX = 0
|
|
||||||
private var previousTouchY = 0
|
|
||||||
private var controlPositionX = 0
|
|
||||||
private var controlPositionY = 0
|
|
||||||
|
|
||||||
private var upButtonState = false
|
|
||||||
private var downButtonState = false
|
|
||||||
private var leftButtonState = false
|
|
||||||
private var rightButtonState = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
|
|
||||||
this.pressedOneDirectionStateBitmap = BitmapDrawable(res, pressedOneDirectionStateBitmap)
|
|
||||||
this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)
|
|
||||||
width = this.defaultStateBitmap.intrinsicWidth
|
|
||||||
height = this.defaultStateBitmap.intrinsicHeight
|
|
||||||
trackId = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateStatus(event: MotionEvent, dpad_slide: Boolean): Boolean {
|
|
||||||
val pointerIndex = event.actionIndex
|
|
||||||
val xPosition = event.getX(pointerIndex).toInt()
|
|
||||||
val yPosition = event.getY(pointerIndex).toInt()
|
|
||||||
val pointerId = event.getPointerId(pointerIndex)
|
|
||||||
val motionEvent = event.action and MotionEvent.ACTION_MASK
|
|
||||||
val isActionDown =
|
|
||||||
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
|
|
||||||
val isActionUp =
|
|
||||||
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
|
|
||||||
if (isActionDown) {
|
|
||||||
if (!bounds.contains(xPosition, yPosition)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
trackId = pointerId
|
|
||||||
}
|
|
||||||
if (isActionUp) {
|
|
||||||
if (trackId != pointerId) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
trackId = -1
|
|
||||||
upButtonState = false
|
|
||||||
downButtonState = false
|
|
||||||
leftButtonState = false
|
|
||||||
rightButtonState = false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (trackId == -1) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (!dpad_slide && !isActionDown) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (i in 0 until event.pointerCount) {
|
|
||||||
if (trackId != event.getPointerId(i)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var touchX = event.getX(i)
|
|
||||||
var touchY = event.getY(i)
|
|
||||||
var maxY = bounds.bottom.toFloat()
|
|
||||||
var maxX = bounds.right.toFloat()
|
|
||||||
touchX -= bounds.centerX().toFloat()
|
|
||||||
maxX -= bounds.centerX().toFloat()
|
|
||||||
touchY -= bounds.centerY().toFloat()
|
|
||||||
maxY -= bounds.centerY().toFloat()
|
|
||||||
val axisX = touchX / maxX
|
|
||||||
val axisY = touchY / maxY
|
|
||||||
val oldUpState = upButtonState
|
|
||||||
val oldDownState = downButtonState
|
|
||||||
val oldLeftState = leftButtonState
|
|
||||||
val oldRightState = rightButtonState
|
|
||||||
|
|
||||||
upButtonState = axisY < -VIRT_AXIS_DEADZONE
|
|
||||||
downButtonState = axisY > VIRT_AXIS_DEADZONE
|
|
||||||
leftButtonState = axisX < -VIRT_AXIS_DEADZONE
|
|
||||||
rightButtonState = axisX > VIRT_AXIS_DEADZONE
|
|
||||||
return oldUpState != upButtonState ||
|
|
||||||
oldDownState != downButtonState ||
|
|
||||||
oldLeftState != leftButtonState ||
|
|
||||||
oldRightState != rightButtonState
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun draw(canvas: Canvas) {
|
|
||||||
val px = controlPositionX + width / 2
|
|
||||||
val py = controlPositionY + height / 2
|
|
||||||
|
|
||||||
// Pressed up
|
|
||||||
if (upButtonState && !leftButtonState && !rightButtonState) {
|
|
||||||
pressedOneDirectionStateBitmap.draw(canvas)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pressed down
|
|
||||||
if (downButtonState && !leftButtonState && !rightButtonState) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.rotate(180f, px.toFloat(), py.toFloat())
|
|
||||||
pressedOneDirectionStateBitmap.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pressed left
|
|
||||||
if (leftButtonState && !upButtonState && !downButtonState) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.rotate(270f, px.toFloat(), py.toFloat())
|
|
||||||
pressedOneDirectionStateBitmap.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pressed right
|
|
||||||
if (rightButtonState && !upButtonState && !downButtonState) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.rotate(90f, px.toFloat(), py.toFloat())
|
|
||||||
pressedOneDirectionStateBitmap.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pressed up left
|
|
||||||
if (upButtonState && leftButtonState && !rightButtonState) {
|
|
||||||
pressedTwoDirectionsStateBitmap.draw(canvas)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pressed up right
|
|
||||||
if (upButtonState && !leftButtonState && rightButtonState) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.rotate(90f, px.toFloat(), py.toFloat())
|
|
||||||
pressedTwoDirectionsStateBitmap.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pressed down right
|
|
||||||
if (downButtonState && !leftButtonState && rightButtonState) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.rotate(180f, px.toFloat(), py.toFloat())
|
|
||||||
pressedTwoDirectionsStateBitmap.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pressed down left
|
|
||||||
if (downButtonState && leftButtonState && !rightButtonState) {
|
|
||||||
canvas.save()
|
|
||||||
canvas.rotate(270f, px.toFloat(), py.toFloat())
|
|
||||||
pressedTwoDirectionsStateBitmap.draw(canvas)
|
|
||||||
canvas.restore()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not pressed
|
|
||||||
defaultStateBitmap.draw(canvas)
|
|
||||||
}
|
|
||||||
|
|
||||||
val upStatus: Int
|
|
||||||
get() = if (upButtonState) ButtonState.PRESSED else ButtonState.RELEASED
|
|
||||||
val downStatus: Int
|
|
||||||
get() = if (downButtonState) ButtonState.PRESSED else ButtonState.RELEASED
|
|
||||||
val leftStatus: Int
|
|
||||||
get() = if (leftButtonState) ButtonState.PRESSED else ButtonState.RELEASED
|
|
||||||
val rightStatus: Int
|
|
||||||
get() = if (rightButtonState) ButtonState.PRESSED else ButtonState.RELEASED
|
|
||||||
|
|
||||||
fun onConfigureTouch(event: MotionEvent): Boolean {
|
|
||||||
val pointerIndex = event.actionIndex
|
|
||||||
val fingerPositionX = event.getX(pointerIndex).toInt()
|
|
||||||
val fingerPositionY = event.getY(pointerIndex).toInt()
|
|
||||||
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
previousTouchX = fingerPositionX
|
|
||||||
previousTouchY = fingerPositionY
|
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
controlPositionX += fingerPositionX - previousTouchX
|
|
||||||
controlPositionY += fingerPositionY - previousTouchY
|
|
||||||
setBounds(
|
|
||||||
controlPositionX,
|
|
||||||
controlPositionY,
|
|
||||||
width + controlPositionX,
|
|
||||||
height + controlPositionY
|
|
||||||
)
|
|
||||||
previousTouchX = fingerPositionX
|
|
||||||
previousTouchY = fingerPositionY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPosition(x: Int, y: Int) {
|
|
||||||
controlPositionX = x
|
|
||||||
controlPositionY = y
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
defaultStateBitmap.setBounds(left, top, right, bottom)
|
|
||||||
pressedOneDirectionStateBitmap.setBounds(left, top, right, bottom)
|
|
||||||
pressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOpacity(value: Int) {
|
|
||||||
defaultStateBitmap.alpha = value
|
|
||||||
pressedOneDirectionStateBitmap.alpha = value
|
|
||||||
pressedTwoDirectionsStateBitmap.alpha = value
|
|
||||||
}
|
|
||||||
|
|
||||||
val bounds: Rect
|
|
||||||
get() = defaultStateBitmap.bounds
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val VIRT_AXIS_DEADZONE = 0.5f
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,188 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.overlay.model
|
|
||||||
|
|
||||||
import androidx.annotation.IntegerRes
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
|
|
||||||
enum class OverlayControl(
|
|
||||||
val id: String,
|
|
||||||
val defaultVisibility: Boolean,
|
|
||||||
@IntegerRes val defaultLandscapePositionResources: Pair<Int, Int>,
|
|
||||||
@IntegerRes val defaultPortraitPositionResources: Pair<Int, Int>,
|
|
||||||
@IntegerRes val defaultFoldablePositionResources: Pair<Int, Int>
|
|
||||||
) {
|
|
||||||
BUTTON_A(
|
|
||||||
"button_a",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_A_X, R.integer.BUTTON_A_Y),
|
|
||||||
Pair(R.integer.BUTTON_A_X_PORTRAIT, R.integer.BUTTON_A_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_B(
|
|
||||||
"button_b",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_B_X, R.integer.BUTTON_B_Y),
|
|
||||||
Pair(R.integer.BUTTON_B_X_PORTRAIT, R.integer.BUTTON_B_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_X(
|
|
||||||
"button_x",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_X_X, R.integer.BUTTON_X_Y),
|
|
||||||
Pair(R.integer.BUTTON_X_X_PORTRAIT, R.integer.BUTTON_X_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_Y(
|
|
||||||
"button_y",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_Y_X, R.integer.BUTTON_Y_Y),
|
|
||||||
Pair(R.integer.BUTTON_Y_X_PORTRAIT, R.integer.BUTTON_Y_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_PLUS(
|
|
||||||
"button_plus",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_PLUS_X, R.integer.BUTTON_PLUS_Y),
|
|
||||||
Pair(R.integer.BUTTON_PLUS_X_PORTRAIT, R.integer.BUTTON_PLUS_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_MINUS(
|
|
||||||
"button_minus",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_MINUS_X, R.integer.BUTTON_MINUS_Y),
|
|
||||||
Pair(R.integer.BUTTON_MINUS_X_PORTRAIT, R.integer.BUTTON_MINUS_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_HOME(
|
|
||||||
"button_home",
|
|
||||||
false,
|
|
||||||
Pair(R.integer.BUTTON_HOME_X, R.integer.BUTTON_HOME_Y),
|
|
||||||
Pair(R.integer.BUTTON_HOME_X_PORTRAIT, R.integer.BUTTON_HOME_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_CAPTURE(
|
|
||||||
"button_capture",
|
|
||||||
false,
|
|
||||||
Pair(R.integer.BUTTON_CAPTURE_X, R.integer.BUTTON_CAPTURE_Y),
|
|
||||||
Pair(R.integer.BUTTON_CAPTURE_X_PORTRAIT, R.integer.BUTTON_CAPTURE_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_L(
|
|
||||||
"button_l",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_L_X, R.integer.BUTTON_L_Y),
|
|
||||||
Pair(R.integer.BUTTON_L_X_PORTRAIT, R.integer.BUTTON_L_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_R(
|
|
||||||
"button_r",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_R_X, R.integer.BUTTON_R_Y),
|
|
||||||
Pair(R.integer.BUTTON_R_X_PORTRAIT, R.integer.BUTTON_R_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_ZL(
|
|
||||||
"button_zl",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_ZL_X, R.integer.BUTTON_ZL_Y),
|
|
||||||
Pair(R.integer.BUTTON_ZL_X_PORTRAIT, R.integer.BUTTON_ZL_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_ZR(
|
|
||||||
"button_zr",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_ZR_X, R.integer.BUTTON_ZR_Y),
|
|
||||||
Pair(R.integer.BUTTON_ZR_X_PORTRAIT, R.integer.BUTTON_ZR_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_STICK_L(
|
|
||||||
"button_stick_l",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_STICK_L_X, R.integer.BUTTON_STICK_L_Y),
|
|
||||||
Pair(R.integer.BUTTON_STICK_L_X_PORTRAIT, R.integer.BUTTON_STICK_L_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
BUTTON_STICK_R(
|
|
||||||
"button_stick_r",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.BUTTON_STICK_R_X, R.integer.BUTTON_STICK_R_Y),
|
|
||||||
Pair(R.integer.BUTTON_STICK_R_X_PORTRAIT, R.integer.BUTTON_STICK_R_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
STICK_L(
|
|
||||||
"stick_l",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.STICK_L_X, R.integer.STICK_L_Y),
|
|
||||||
Pair(R.integer.STICK_L_X_PORTRAIT, R.integer.STICK_L_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
STICK_R(
|
|
||||||
"stick_r",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.STICK_R_X, R.integer.STICK_R_Y),
|
|
||||||
Pair(R.integer.STICK_R_X_PORTRAIT, R.integer.STICK_R_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE)
|
|
||||||
),
|
|
||||||
COMBINED_DPAD(
|
|
||||||
"combined_dpad",
|
|
||||||
true,
|
|
||||||
Pair(R.integer.COMBINED_DPAD_X, R.integer.COMBINED_DPAD_Y),
|
|
||||||
Pair(R.integer.COMBINED_DPAD_X_PORTRAIT, R.integer.COMBINED_DPAD_Y_PORTRAIT),
|
|
||||||
Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE)
|
|
||||||
);
|
|
||||||
|
|
||||||
fun getDefaultPositionForLayout(layout: OverlayLayout): Pair<Double, Double> {
|
|
||||||
val rawResourcePair: Pair<Int, Int>
|
|
||||||
SudachiApplication.appContext.resources.apply {
|
|
||||||
rawResourcePair = when (layout) {
|
|
||||||
OverlayLayout.Landscape -> {
|
|
||||||
Pair(
|
|
||||||
getInteger(this@OverlayControl.defaultLandscapePositionResources.first),
|
|
||||||
getInteger(this@OverlayControl.defaultLandscapePositionResources.second)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayLayout.Portrait -> {
|
|
||||||
Pair(
|
|
||||||
getInteger(this@OverlayControl.defaultPortraitPositionResources.first),
|
|
||||||
getInteger(this@OverlayControl.defaultPortraitPositionResources.second)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayLayout.Foldable -> {
|
|
||||||
Pair(
|
|
||||||
getInteger(this@OverlayControl.defaultFoldablePositionResources.first),
|
|
||||||
getInteger(this@OverlayControl.defaultFoldablePositionResources.second)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pair(
|
|
||||||
rawResourcePair.first.toDouble() / 1000,
|
|
||||||
rawResourcePair.second.toDouble() / 1000
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toOverlayControlData(): OverlayControlData =
|
|
||||||
OverlayControlData(
|
|
||||||
id,
|
|
||||||
defaultVisibility,
|
|
||||||
getDefaultPositionForLayout(OverlayLayout.Landscape),
|
|
||||||
getDefaultPositionForLayout(OverlayLayout.Portrait),
|
|
||||||
getDefaultPositionForLayout(OverlayLayout.Foldable)
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val map: HashMap<String, OverlayControl> by lazy {
|
|
||||||
val hashMap = hashMapOf<String, OverlayControl>()
|
|
||||||
entries.forEach { hashMap[it.id] = it }
|
|
||||||
hashMap
|
|
||||||
}
|
|
||||||
|
|
||||||
fun from(id: String): OverlayControl? = map[id]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.overlay.model
|
|
||||||
|
|
||||||
data class OverlayControlData(
|
|
||||||
val id: String,
|
|
||||||
var enabled: Boolean,
|
|
||||||
var landscapePosition: Pair<Double, Double>,
|
|
||||||
var portraitPosition: Pair<Double, Double>,
|
|
||||||
var foldablePosition: Pair<Double, Double>
|
|
||||||
) {
|
|
||||||
fun positionFromLayout(layout: OverlayLayout): Pair<Double, Double> =
|
|
||||||
when (layout) {
|
|
||||||
OverlayLayout.Landscape -> landscapePosition
|
|
||||||
OverlayLayout.Portrait -> portraitPosition
|
|
||||||
OverlayLayout.Foldable -> foldablePosition
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,692 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.ui.main
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.animation.PathInterpolator
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import androidx.navigation.ui.setupWithNavController
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FilenameFilter
|
|
||||||
import org.sudachi.sudachi_emu.HomeNavigationDirections
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.databinding.ActivityMainBinding
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.Settings
|
|
||||||
import org.sudachi.sudachi_emu.fragments.AddGameFolderDialogFragment
|
|
||||||
import org.sudachi.sudachi_emu.fragments.ProgressDialogFragment
|
|
||||||
import org.sudachi.sudachi_emu.fragments.MessageDialogFragment
|
|
||||||
import org.sudachi.sudachi_emu.model.AddonViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.DriverViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.GamesViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.HomeViewModel
|
|
||||||
import org.sudachi.sudachi_emu.model.InstallResult
|
|
||||||
import org.sudachi.sudachi_emu.model.TaskState
|
|
||||||
import org.sudachi.sudachi_emu.model.TaskViewModel
|
|
||||||
import org.sudachi.sudachi_emu.utils.*
|
|
||||||
import org.sudachi.sudachi_emu.utils.ViewUtils.setVisible
|
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ThemeProvider {
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
|
||||||
|
|
||||||
private val homeViewModel: HomeViewModel by viewModels()
|
|
||||||
private val gamesViewModel: GamesViewModel by viewModels()
|
|
||||||
private val taskViewModel: TaskViewModel by viewModels()
|
|
||||||
private val addonViewModel: AddonViewModel by viewModels()
|
|
||||||
private val driverViewModel: DriverViewModel by viewModels()
|
|
||||||
|
|
||||||
override var themeId: Int = 0
|
|
||||||
|
|
||||||
private val CHECKED_DECRYPTION = "CheckedDecryption"
|
|
||||||
private var checkedDecryption = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
val splashScreen = installSplashScreen()
|
|
||||||
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
|
|
||||||
|
|
||||||
ThemeHelper.setTheme(this)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
checkedDecryption = savedInstanceState.getBoolean(CHECKED_DECRYPTION)
|
|
||||||
}
|
|
||||||
if (!checkedDecryption) {
|
|
||||||
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
|
||||||
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
|
||||||
if (!firstTimeSetup) {
|
|
||||||
checkKeys()
|
|
||||||
}
|
|
||||||
checkedDecryption = true
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
|
||||||
|
|
||||||
window.statusBarColor =
|
|
||||||
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
|
||||||
window.navigationBarColor =
|
|
||||||
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
|
||||||
|
|
||||||
binding.statusBarShade.setBackgroundColor(
|
|
||||||
ThemeHelper.getColorWithOpacity(
|
|
||||||
MaterialColors.getColor(
|
|
||||||
binding.root,
|
|
||||||
com.google.android.material.R.attr.colorSurface
|
|
||||||
),
|
|
||||||
ThemeHelper.SYSTEM_BAR_ALPHA
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
|
||||||
InsetsHelper.GESTURE_NAVIGATION
|
|
||||||
) {
|
|
||||||
binding.navigationBarShade.setBackgroundColor(
|
|
||||||
ThemeHelper.getColorWithOpacity(
|
|
||||||
MaterialColors.getColor(
|
|
||||||
binding.root,
|
|
||||||
com.google.android.material.R.attr.colorSurface
|
|
||||||
),
|
|
||||||
ThemeHelper.SYSTEM_BAR_ALPHA
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val navHostFragment =
|
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
|
||||||
setUpNavigation(navHostFragment.navController)
|
|
||||||
(binding.navigationView as NavigationBarView).setOnItemReselectedListener {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
|
|
||||||
R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
|
|
||||||
R.id.homeSettingsFragment -> {
|
|
||||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
|
||||||
null,
|
|
||||||
Settings.MenuTag.SECTION_ROOT
|
|
||||||
)
|
|
||||||
navHostFragment.navController.navigate(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevents navigation from being drawn for a short time on recreation if set to hidden
|
|
||||||
if (!homeViewModel.navigationVisible.value.first) {
|
|
||||||
binding.navigationView.setVisible(visible = false, gone = false)
|
|
||||||
binding.statusBarShade.setVisible(visible = false, gone = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
homeViewModel.navigationVisible.collect(this) { showNavigation(it.first, it.second) }
|
|
||||||
homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) }
|
|
||||||
homeViewModel.contentToInstall.collect(
|
|
||||||
this,
|
|
||||||
resetState = { homeViewModel.setContentToInstall(null) }
|
|
||||||
) {
|
|
||||||
if (it != null) {
|
|
||||||
installContent(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
homeViewModel.checkKeys.collect(this, resetState = { homeViewModel.setCheckKeys(false) }) {
|
|
||||||
if (it) checkKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkKeys() {
|
|
||||||
if (!NativeLibrary.areKeysPresent()) {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
titleId = R.string.keys_missing,
|
|
||||||
descriptionId = R.string.keys_missing_description,
|
|
||||||
helpLinkId = R.string.keys_missing_help
|
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putBoolean(CHECKED_DECRYPTION, checkedDecryption)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun finishSetup(navController: NavController) {
|
|
||||||
navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
|
|
||||||
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
|
|
||||||
showNavigation(visible = true, animated = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUpNavigation(navController: NavController) {
|
|
||||||
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
|
||||||
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
|
||||||
|
|
||||||
if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
|
|
||||||
navController.navigate(R.id.firstTimeSetupFragment)
|
|
||||||
homeViewModel.navigatedToSetup = true
|
|
||||||
} else {
|
|
||||||
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNavigation(visible: Boolean, animated: Boolean) {
|
|
||||||
if (!animated) {
|
|
||||||
binding.navigationView.setVisible(visible)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val smallLayout = resources.getBoolean(R.bool.small_layout)
|
|
||||||
binding.navigationView.animate().apply {
|
|
||||||
if (visible) {
|
|
||||||
binding.navigationView.setVisible(true)
|
|
||||||
duration = 300
|
|
||||||
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
|
|
||||||
|
|
||||||
if (smallLayout) {
|
|
||||||
binding.navigationView.translationY =
|
|
||||||
binding.navigationView.height.toFloat() * 2
|
|
||||||
translationY(0f)
|
|
||||||
} else {
|
|
||||||
if (ViewCompat.getLayoutDirection(binding.navigationView) ==
|
|
||||||
ViewCompat.LAYOUT_DIRECTION_LTR
|
|
||||||
) {
|
|
||||||
binding.navigationView.translationX =
|
|
||||||
binding.navigationView.width.toFloat() * -2
|
|
||||||
translationX(0f)
|
|
||||||
} else {
|
|
||||||
binding.navigationView.translationX =
|
|
||||||
binding.navigationView.width.toFloat() * 2
|
|
||||||
translationX(0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
duration = 300
|
|
||||||
interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
|
|
||||||
|
|
||||||
if (smallLayout) {
|
|
||||||
translationY(binding.navigationView.height.toFloat() * 2)
|
|
||||||
} else {
|
|
||||||
if (ViewCompat.getLayoutDirection(binding.navigationView) ==
|
|
||||||
ViewCompat.LAYOUT_DIRECTION_LTR
|
|
||||||
) {
|
|
||||||
translationX(binding.navigationView.width.toFloat() * -2)
|
|
||||||
} else {
|
|
||||||
translationX(binding.navigationView.width.toFloat() * 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.withEndAction {
|
|
||||||
if (!visible) {
|
|
||||||
binding.navigationView.setVisible(visible = false, gone = false)
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showStatusBarShade(visible: Boolean) {
|
|
||||||
binding.statusBarShade.animate().apply {
|
|
||||||
if (visible) {
|
|
||||||
binding.statusBarShade.setVisible(true)
|
|
||||||
binding.statusBarShade.translationY = binding.statusBarShade.height.toFloat() * -2
|
|
||||||
duration = 300
|
|
||||||
translationY(0f)
|
|
||||||
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
|
|
||||||
} else {
|
|
||||||
duration = 300
|
|
||||||
translationY(binding.navigationView.height.toFloat() * -2)
|
|
||||||
interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
|
|
||||||
}
|
|
||||||
}.withEndAction {
|
|
||||||
if (!visible) {
|
|
||||||
binding.statusBarShade.setVisible(visible = false, gone = false)
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
ThemeHelper.setCorrectTheme(this)
|
|
||||||
super.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val mlpStatusShade = binding.statusBarShade.layoutParams as MarginLayoutParams
|
|
||||||
mlpStatusShade.height = insets.top
|
|
||||||
binding.statusBarShade.layoutParams = mlpStatusShade
|
|
||||||
|
|
||||||
// The only situation where we care to have a nav bar shade is when it's at the bottom
|
|
||||||
// of the screen where scrolling list elements can go behind it.
|
|
||||||
val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
|
|
||||||
mlpNavShade.height = insets.bottom
|
|
||||||
binding.navigationBarShade.layoutParams = mlpNavShade
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setTheme(resId: Int) {
|
|
||||||
super.setTheme(resId)
|
|
||||||
themeId = resId
|
|
||||||
}
|
|
||||||
|
|
||||||
val getGamesDirectory =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
|
||||||
if (result != null) {
|
|
||||||
processGamesDir(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processGamesDir(result: Uri) {
|
|
||||||
contentResolver.takePersistableUriPermission(
|
|
||||||
result,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
)
|
|
||||||
|
|
||||||
val uriString = result.toString()
|
|
||||||
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
|
||||||
if (folder != null) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
R.string.folder_already_added,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
AddGameFolderDialogFragment.newInstance(uriString)
|
|
||||||
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
val getProdKey =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
||||||
if (result != null) {
|
|
||||||
processKey(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processKey(result: Uri): Boolean {
|
|
||||||
if (FileUtil.getExtension(result) != "keys") {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.reading_keys_failure,
|
|
||||||
descriptionId = R.string.install_prod_keys_failure_extension_description
|
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
contentResolver.takePersistableUriPermission(
|
|
||||||
result,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
)
|
|
||||||
|
|
||||||
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
|
|
||||||
if (FileUtil.copyUriToInternalStorage(
|
|
||||||
result,
|
|
||||||
dstPath,
|
|
||||||
"prod.keys"
|
|
||||||
) != null
|
|
||||||
) {
|
|
||||||
if (NativeLibrary.reloadKeys()) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
R.string.install_keys_success,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
homeViewModel.setCheckKeys(true)
|
|
||||||
gamesViewModel.reloadGames(true)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.invalid_keys_error,
|
|
||||||
descriptionId = R.string.install_keys_failure_description,
|
|
||||||
helpLinkId = R.string.dumping_keys_quickstart_link
|
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val getFirmware =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
|
|
||||||
|
|
||||||
val firmwarePath =
|
|
||||||
File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
|
|
||||||
val cacheFirmwareDir = File("${cacheDir.path}/registered/")
|
|
||||||
|
|
||||||
ProgressDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
R.string.firmware_installing
|
|
||||||
) { progressCallback, _ ->
|
|
||||||
var messageToShow: Any
|
|
||||||
try {
|
|
||||||
FileUtil.unzipToInternalStorage(
|
|
||||||
result.toString(),
|
|
||||||
cacheFirmwareDir,
|
|
||||||
progressCallback
|
|
||||||
)
|
|
||||||
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
|
|
||||||
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
|
|
||||||
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.firmware_installed_failure,
|
|
||||||
descriptionId = R.string.firmware_installed_failure_description
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
firmwarePath.deleteRecursively()
|
|
||||||
cacheFirmwareDir.copyRecursively(firmwarePath, true)
|
|
||||||
NativeLibrary.initializeSystem(true)
|
|
||||||
homeViewModel.setCheckKeys(true)
|
|
||||||
getString(R.string.save_file_imported_success)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[MainActivity] Firmware install failed - ${e.message}")
|
|
||||||
messageToShow = getString(R.string.fatal_error)
|
|
||||||
} finally {
|
|
||||||
cacheFirmwareDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
messageToShow
|
|
||||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
val getAmiiboKey =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FileUtil.getExtension(result) != "bin") {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.reading_keys_failure,
|
|
||||||
descriptionId = R.string.install_amiibo_keys_failure_extension_description
|
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
contentResolver.takePersistableUriPermission(
|
|
||||||
result,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
)
|
|
||||||
|
|
||||||
val dstPath = DirectoryInitialization.userDirectory + "/keys/"
|
|
||||||
if (FileUtil.copyUriToInternalStorage(
|
|
||||||
result,
|
|
||||||
dstPath,
|
|
||||||
"key_retail.bin"
|
|
||||||
) != null
|
|
||||||
) {
|
|
||||||
if (NativeLibrary.reloadKeys()) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
R.string.install_keys_success,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
} else {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.invalid_keys_error,
|
|
||||||
descriptionId = R.string.install_keys_failure_description,
|
|
||||||
helpLinkId = R.string.dumping_keys_quickstart_link
|
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val installGameUpdate = registerForActivityResult(
|
|
||||||
ActivityResultContracts.OpenMultipleDocuments()
|
|
||||||
) { documents: List<Uri> ->
|
|
||||||
if (documents.isEmpty()) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addonViewModel.game == null) {
|
|
||||||
installContent(documents)
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressDialogFragment.newInstance(
|
|
||||||
this@MainActivity,
|
|
||||||
R.string.verifying_content,
|
|
||||||
false
|
|
||||||
) { _, _ ->
|
|
||||||
var updatesMatchProgram = true
|
|
||||||
for (document in documents) {
|
|
||||||
val valid = NativeLibrary.doesUpdateMatchProgram(
|
|
||||||
addonViewModel.game!!.programId,
|
|
||||||
document.toString()
|
|
||||||
)
|
|
||||||
if (!valid) {
|
|
||||||
updatesMatchProgram = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatesMatchProgram) {
|
|
||||||
homeViewModel.setContentToInstall(documents)
|
|
||||||
} else {
|
|
||||||
MessageDialogFragment.newInstance(
|
|
||||||
this@MainActivity,
|
|
||||||
titleId = R.string.content_install_notice,
|
|
||||||
descriptionId = R.string.content_install_notice_description,
|
|
||||||
positiveAction = { homeViewModel.setContentToInstall(documents) },
|
|
||||||
negativeAction = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun installContent(documents: List<Uri>) {
|
|
||||||
ProgressDialogFragment.newInstance(
|
|
||||||
this@MainActivity,
|
|
||||||
R.string.installing_game_content
|
|
||||||
) { progressCallback, messageCallback ->
|
|
||||||
var installSuccess = 0
|
|
||||||
var installOverwrite = 0
|
|
||||||
var errorBaseGame = 0
|
|
||||||
var error = 0
|
|
||||||
documents.forEach {
|
|
||||||
messageCallback.invoke(FileUtil.getFilename(it))
|
|
||||||
when (
|
|
||||||
InstallResult.from(
|
|
||||||
NativeLibrary.installFileToNand(
|
|
||||||
it.toString(),
|
|
||||||
progressCallback
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
InstallResult.Success -> {
|
|
||||||
installSuccess += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallResult.Overwrite -> {
|
|
||||||
installOverwrite += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallResult.BaseInstallAttempted -> {
|
|
||||||
errorBaseGame += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallResult.Failure -> {
|
|
||||||
error += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addonViewModel.refreshAddons()
|
|
||||||
|
|
||||||
val separator = System.getProperty("line.separator") ?: "\n"
|
|
||||||
val installResult = StringBuilder()
|
|
||||||
if (installSuccess > 0) {
|
|
||||||
installResult.append(
|
|
||||||
getString(
|
|
||||||
R.string.install_game_content_success_install,
|
|
||||||
installSuccess
|
|
||||||
)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
if (installOverwrite > 0) {
|
|
||||||
installResult.append(
|
|
||||||
getString(
|
|
||||||
R.string.install_game_content_success_overwrite,
|
|
||||||
installOverwrite
|
|
||||||
)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
val errorTotal: Int = errorBaseGame + error
|
|
||||||
if (errorTotal > 0) {
|
|
||||||
installResult.append(separator)
|
|
||||||
installResult.append(
|
|
||||||
getString(
|
|
||||||
R.string.install_game_content_failed_count,
|
|
||||||
errorTotal
|
|
||||||
)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
if (errorBaseGame > 0) {
|
|
||||||
installResult.append(separator)
|
|
||||||
installResult.append(
|
|
||||||
getString(R.string.install_game_content_failure_base)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
if (error > 0) {
|
|
||||||
installResult.append(
|
|
||||||
getString(R.string.install_game_content_failure_description)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.install_game_content_failure,
|
|
||||||
descriptionString = installResult.toString().trim(),
|
|
||||||
helpLinkId = R.string.install_game_content_help_link
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.install_game_content_success,
|
|
||||||
descriptionString = installResult.toString().trim()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
val exportUserData = registerForActivityResult(
|
|
||||||
ActivityResultContracts.CreateDocument("application/zip")
|
|
||||||
) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
R.string.exporting_user_data,
|
|
||||||
true
|
|
||||||
) { progressCallback, _ ->
|
|
||||||
val zipResult = FileUtil.zipFromInternalStorage(
|
|
||||||
File(DirectoryInitialization.userDirectory!!),
|
|
||||||
DirectoryInitialization.userDirectory!!,
|
|
||||||
BufferedOutputStream(contentResolver.openOutputStream(result)),
|
|
||||||
progressCallback,
|
|
||||||
compression = false
|
|
||||||
)
|
|
||||||
return@newInstance when (zipResult) {
|
|
||||||
TaskState.Completed -> getString(R.string.user_data_export_success)
|
|
||||||
TaskState.Failed -> R.string.export_failed
|
|
||||||
TaskState.Cancelled -> R.string.user_data_export_cancelled
|
|
||||||
}
|
|
||||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
|
|
||||||
val importUserData =
|
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
||||||
if (result == null) {
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
R.string.importing_user_data
|
|
||||||
) { progressCallback, _ ->
|
|
||||||
val checkStream =
|
|
||||||
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
|
|
||||||
var isSudachiBackup = false
|
|
||||||
checkStream.use { stream ->
|
|
||||||
var ze: ZipEntry? = null
|
|
||||||
while (stream.nextEntry?.also { ze = it } != null) {
|
|
||||||
val itemName = ze!!.name.trim()
|
|
||||||
if (itemName == "/config/config.ini" || itemName == "config/config.ini") {
|
|
||||||
isSudachiBackup = true
|
|
||||||
return@use
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isSudachiBackup) {
|
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.invalid_sudachi_backup,
|
|
||||||
descriptionId = R.string.user_data_import_failed_description
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing user data
|
|
||||||
NativeConfig.unloadGlobalConfig()
|
|
||||||
File(DirectoryInitialization.userDirectory!!).deleteRecursively()
|
|
||||||
|
|
||||||
// Copy archive to internal storage
|
|
||||||
try {
|
|
||||||
FileUtil.unzipToInternalStorage(
|
|
||||||
result.toString(),
|
|
||||||
File(DirectoryInitialization.userDirectory!!),
|
|
||||||
progressCallback
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
titleId = R.string.import_failed,
|
|
||||||
descriptionId = R.string.user_data_import_failed_description
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reinitialize relevant data
|
|
||||||
NativeLibrary.initializeSystem(true)
|
|
||||||
NativeConfig.initializeGlobalConfig()
|
|
||||||
gamesViewModel.reloadGames(false)
|
|
||||||
driverViewModel.reloadDriverData()
|
|
||||||
|
|
||||||
return@newInstance getString(R.string.user_data_import_success)
|
|
||||||
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.ui.main
|
|
||||||
|
|
||||||
interface ThemeProvider {
|
|
||||||
/**
|
|
||||||
* Provides theme ID by overriding an activity's 'setTheme' method and returning that result
|
|
||||||
*/
|
|
||||||
var themeId: Int
|
|
||||||
}
|
|
|
@ -1,503 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.URLDecoder
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.model.MinimalDocumentFile
|
|
||||||
import org.sudachi.sudachi_emu.model.TaskState
|
|
||||||
import java.io.BufferedOutputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.lang.NullPointerException
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.util.zip.Deflater
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
import kotlin.IllegalStateException
|
|
||||||
|
|
||||||
object FileUtil {
|
|
||||||
const val PATH_TREE = "tree"
|
|
||||||
const val DECODE_METHOD = "UTF-8"
|
|
||||||
const val APPLICATION_OCTET_STREAM = "application/octet-stream"
|
|
||||||
const val TEXT_PLAIN = "text/plain"
|
|
||||||
|
|
||||||
private val context get() = SudachiApplication.appContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a file from directory with filename.
|
|
||||||
* @param context Application context
|
|
||||||
* @param directory parent path for file.
|
|
||||||
* @param filename file display name.
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
fun createFile(directory: String?, filename: String): DocumentFile? {
|
|
||||||
var decodedFilename = filename
|
|
||||||
try {
|
|
||||||
val directoryUri = Uri.parse(directory)
|
|
||||||
val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
|
|
||||||
decodedFilename = URLDecoder.decode(decodedFilename, DECODE_METHOD)
|
|
||||||
var mimeType = APPLICATION_OCTET_STREAM
|
|
||||||
if (decodedFilename.endsWith(".txt")) {
|
|
||||||
mimeType = TEXT_PLAIN
|
|
||||||
}
|
|
||||||
val exists = parent.findFile(decodedFilename)
|
|
||||||
return exists ?: parent.createFile(mimeType, decodedFilename)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil]: Cannot create file, error: " + e.message)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a directory from directory with filename.
|
|
||||||
* @param directory parent path for directory.
|
|
||||||
* @param directoryName directory display name.
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
fun createDir(directory: String?, directoryName: String?): DocumentFile? {
|
|
||||||
var decodedDirectoryName = directoryName
|
|
||||||
try {
|
|
||||||
val directoryUri = Uri.parse(directory)
|
|
||||||
val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
|
|
||||||
decodedDirectoryName = URLDecoder.decode(decodedDirectoryName, DECODE_METHOD)
|
|
||||||
val isExist = parent.findFile(decodedDirectoryName)
|
|
||||||
return isExist ?: parent.createDirectory(decodedDirectoryName)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil]: Cannot create file, error: " + e.message)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open content uri and return file descriptor to JNI.
|
|
||||||
* @param path Native content uri path
|
|
||||||
* @param openMode will be one of "r", "r", "rw", "wa", "rwa"
|
|
||||||
* @return file descriptor
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun openContentUri(path: String, openMode: String?): Int {
|
|
||||||
try {
|
|
||||||
val uri = Uri.parse(path)
|
|
||||||
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, openMode!!)
|
|
||||||
if (parcelFileDescriptor == null) {
|
|
||||||
Log.error("[FileUtil]: Cannot get the file descriptor from uri: $path")
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
val fileDescriptor = parcelFileDescriptor.detachFd()
|
|
||||||
parcelFileDescriptor.close()
|
|
||||||
return fileDescriptor
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil]: Cannot open content uri, error: " + e.message)
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
|
|
||||||
* This function will be faster than DocumentFile.listFiles
|
|
||||||
* @param uri Directory uri.
|
|
||||||
* @return CheapDocument lists.
|
|
||||||
*/
|
|
||||||
fun listFiles(uri: Uri): Array<MinimalDocumentFile> {
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val columns = arrayOf(
|
|
||||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
|
||||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
|
||||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
|
||||||
)
|
|
||||||
var c: Cursor? = null
|
|
||||||
val results: MutableList<MinimalDocumentFile> = ArrayList()
|
|
||||||
try {
|
|
||||||
val docId: String = if (isRootTreeUri(uri)) {
|
|
||||||
DocumentsContract.getTreeDocumentId(uri)
|
|
||||||
} else {
|
|
||||||
DocumentsContract.getDocumentId(uri)
|
|
||||||
}
|
|
||||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
|
|
||||||
c = resolver.query(childrenUri, columns, null, null, null)
|
|
||||||
while (c!!.moveToNext()) {
|
|
||||||
val documentId = c.getString(0)
|
|
||||||
val documentName = c.getString(1)
|
|
||||||
val documentMimeType = c.getString(2)
|
|
||||||
val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId)
|
|
||||||
val document = MinimalDocumentFile(documentName, documentMimeType, documentUri)
|
|
||||||
results.add(document)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil]: Cannot list file error: " + e.message)
|
|
||||||
} finally {
|
|
||||||
closeQuietly(c)
|
|
||||||
}
|
|
||||||
return results.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether given path exists.
|
|
||||||
* @param path Native content uri path
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
fun exists(path: String?, suppressLog: Boolean = false): Boolean {
|
|
||||||
var c: Cursor? = null
|
|
||||||
try {
|
|
||||||
val mUri = Uri.parse(path)
|
|
||||||
val columns = arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
|
||||||
c = context.contentResolver.query(mUri, columns, null, null, null)
|
|
||||||
return c!!.count > 0
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (!suppressLog) {
|
|
||||||
Log.info("[FileUtil] Cannot find file from given path, error: " + e.message)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
closeQuietly(c)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether given path is a directory
|
|
||||||
* @param path content uri path
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
fun isDirectory(path: String): Boolean {
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val columns = arrayOf(
|
|
||||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
|
||||||
)
|
|
||||||
var isDirectory = false
|
|
||||||
var c: Cursor? = null
|
|
||||||
try {
|
|
||||||
val mUri = Uri.parse(path)
|
|
||||||
c = resolver.query(mUri, columns, null, null, null)
|
|
||||||
c!!.moveToNext()
|
|
||||||
val mimeType = c.getString(0)
|
|
||||||
isDirectory = mimeType == DocumentsContract.Document.MIME_TYPE_DIR
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil]: Cannot list files, error: " + e.message)
|
|
||||||
} finally {
|
|
||||||
closeQuietly(c)
|
|
||||||
}
|
|
||||||
return isDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get file display name from given path
|
|
||||||
* @param uri content uri
|
|
||||||
* @return String display name
|
|
||||||
*/
|
|
||||||
fun getFilename(uri: Uri): String {
|
|
||||||
val resolver = SudachiApplication.appContext.contentResolver
|
|
||||||
val columns = arrayOf(
|
|
||||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME
|
|
||||||
)
|
|
||||||
var filename = ""
|
|
||||||
var c: Cursor? = null
|
|
||||||
try {
|
|
||||||
c = resolver.query(uri, columns, null, null, null)
|
|
||||||
c!!.moveToNext()
|
|
||||||
filename = c.getString(0)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil]: Cannot get file size, error: " + e.message)
|
|
||||||
} finally {
|
|
||||||
closeQuietly(c)
|
|
||||||
}
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFilesName(path: String): Array<String> {
|
|
||||||
val uri = Uri.parse(path)
|
|
||||||
val files: MutableList<String> = ArrayList()
|
|
||||||
for (file in listFiles(uri)) {
|
|
||||||
files.add(file.filename)
|
|
||||||
}
|
|
||||||
return files.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get file size from given path.
|
|
||||||
* @param path content uri path
|
|
||||||
* @return long file size
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun getFileSize(path: String): Long {
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val columns = arrayOf(
|
|
||||||
DocumentsContract.Document.COLUMN_SIZE
|
|
||||||
)
|
|
||||||
var size: Long = 0
|
|
||||||
var c: Cursor? = null
|
|
||||||
try {
|
|
||||||
val mUri = Uri.parse(path)
|
|
||||||
c = resolver.query(mUri, columns, null, null, null)
|
|
||||||
c!!.moveToNext()
|
|
||||||
size = c.getLong(0)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil]: Cannot get file size, error: " + e.message)
|
|
||||||
} finally {
|
|
||||||
closeQuietly(c)
|
|
||||||
}
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an input stream with a given [Uri] and copies its data to the given path. This will
|
|
||||||
* overwrite any pre-existing files.
|
|
||||||
*
|
|
||||||
* @param sourceUri The [Uri] to copy data from
|
|
||||||
* @param destinationParentPath Destination directory
|
|
||||||
* @param destinationFilename Optionally renames the file once copied
|
|
||||||
*/
|
|
||||||
fun copyUriToInternalStorage(
|
|
||||||
sourceUri: Uri,
|
|
||||||
destinationParentPath: String,
|
|
||||||
destinationFilename: String = ""
|
|
||||||
): File? =
|
|
||||||
try {
|
|
||||||
val fileName =
|
|
||||||
if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename"
|
|
||||||
val inputStream = context.contentResolver.openInputStream(sourceUri)!!
|
|
||||||
|
|
||||||
val destinationFile = File("$destinationParentPath$fileName")
|
|
||||||
if (destinationFile.exists()) {
|
|
||||||
destinationFile.delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
destinationFile.outputStream().use { fos ->
|
|
||||||
inputStream.use { it.copyTo(fos) }
|
|
||||||
}
|
|
||||||
destinationFile
|
|
||||||
} catch (e: IOException) {
|
|
||||||
null
|
|
||||||
} catch (e: NullPointerException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the given zip file into the given directory.
|
|
||||||
* @param path String representation of a [Uri] or a typical path delimited by '/'
|
|
||||||
* @param destDir Location to unzip the contents of [path] into
|
|
||||||
* @param progressCallback Lambda that is called with the total number of files and the current
|
|
||||||
* progress through the process. Stops execution as soon as possible if this returns true.
|
|
||||||
*/
|
|
||||||
@Throws(SecurityException::class)
|
|
||||||
fun unzipToInternalStorage(
|
|
||||||
path: String,
|
|
||||||
destDir: File,
|
|
||||||
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
|
|
||||||
) {
|
|
||||||
var totalEntries = 0L
|
|
||||||
ZipInputStream(getInputStream(path)).use { zis ->
|
|
||||||
var tempEntry = zis.nextEntry
|
|
||||||
while (tempEntry != null) {
|
|
||||||
tempEntry = zis.nextEntry
|
|
||||||
totalEntries++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var progress = 0L
|
|
||||||
ZipInputStream(getInputStream(path)).use { zis ->
|
|
||||||
var entry: ZipEntry? = zis.nextEntry
|
|
||||||
while (entry != null) {
|
|
||||||
if (progressCallback.invoke(totalEntries, progress)) {
|
|
||||||
return@use
|
|
||||||
}
|
|
||||||
|
|
||||||
val newFile = File(destDir, entry.name)
|
|
||||||
val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
|
|
||||||
|
|
||||||
if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
|
|
||||||
throw SecurityException("Zip file attempted path traversal! ${entry.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
|
|
||||||
throw IOException("Failed to create directory $destinationDirectory")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.isDirectory) {
|
|
||||||
newFile.outputStream().use { fos -> zis.copyTo(fos) }
|
|
||||||
}
|
|
||||||
entry = zis.nextEntry
|
|
||||||
progress++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a zip file from a directory within internal storage
|
|
||||||
* @param inputFile File representation of the item that will be zipped
|
|
||||||
* @param rootDir Directory containing the inputFile
|
|
||||||
* @param outputStream Stream where the zip file will be output
|
|
||||||
* @param progressCallback Lambda that is called with the total number of files and the current
|
|
||||||
* progress through the process. Stops execution as soon as possible if this returns true.
|
|
||||||
* @param compression Disables compression if true
|
|
||||||
*/
|
|
||||||
fun zipFromInternalStorage(
|
|
||||||
inputFile: File,
|
|
||||||
rootDir: String,
|
|
||||||
outputStream: BufferedOutputStream,
|
|
||||||
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false },
|
|
||||||
compression: Boolean = true
|
|
||||||
): TaskState {
|
|
||||||
try {
|
|
||||||
ZipOutputStream(outputStream).use { zos ->
|
|
||||||
if (!compression) {
|
|
||||||
zos.setMethod(ZipOutputStream.DEFLATED)
|
|
||||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 0L
|
|
||||||
val totalFiles = inputFile.walkTopDown().count().toLong()
|
|
||||||
inputFile.walkTopDown().forEach { file ->
|
|
||||||
if (progressCallback.invoke(totalFiles, count)) {
|
|
||||||
return TaskState.Cancelled
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.isDirectory) {
|
|
||||||
val entryName =
|
|
||||||
file.absolutePath.removePrefix(rootDir).removePrefix("/")
|
|
||||||
val entry = ZipEntry(entryName)
|
|
||||||
zos.putNextEntry(entry)
|
|
||||||
if (file.isFile) {
|
|
||||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.error("[FileUtil] Failed creating zip file - ${e.message}")
|
|
||||||
return TaskState.Failed
|
|
||||||
}
|
|
||||||
return TaskState.Completed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function that copies the contents of a DocumentFile folder into a [File]
|
|
||||||
* @param file [File] representation of the folder to copy into
|
|
||||||
* @param progressCallback Lambda that is called with the total number of files and the current
|
|
||||||
* progress through the process. Stops execution as soon as possible if this returns true.
|
|
||||||
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
|
|
||||||
*/
|
|
||||||
fun DocumentFile.copyFilesTo(
|
|
||||||
file: File,
|
|
||||||
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
|
|
||||||
) {
|
|
||||||
file.mkdirs()
|
|
||||||
if (!this.isDirectory || !file.isDirectory) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"[FileUtil] Tried to copy a folder into a file or vice versa"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 0L
|
|
||||||
val totalFiles = this.listFiles().size.toLong()
|
|
||||||
this.listFiles().forEach {
|
|
||||||
if (progressCallback.invoke(totalFiles, count)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val newFile = File(file, it.name!!)
|
|
||||||
if (it.isDirectory) {
|
|
||||||
newFile.mkdirs()
|
|
||||||
DocumentFile.fromTreeUri(SudachiApplication.appContext, it.uri)?.copyFilesTo(newFile)
|
|
||||||
} else {
|
|
||||||
val inputStream =
|
|
||||||
SudachiApplication.appContext.contentResolver.openInputStream(it.uri)
|
|
||||||
BufferedInputStream(inputStream).use { bos ->
|
|
||||||
if (!newFile.exists()) {
|
|
||||||
newFile.createNewFile()
|
|
||||||
}
|
|
||||||
newFile.outputStream().use { os -> bos.copyTo(os) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isRootTreeUri(uri: Uri): Boolean {
|
|
||||||
val paths = uri.pathSegments
|
|
||||||
return paths.size == 2 && PATH_TREE == paths[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeQuietly(closeable: AutoCloseable?) {
|
|
||||||
if (closeable != null) {
|
|
||||||
try {
|
|
||||||
closeable.close()
|
|
||||||
} catch (rethrown: RuntimeException) {
|
|
||||||
throw rethrown
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getExtension(uri: Uri): String {
|
|
||||||
val fileName = getFilename(uri)
|
|
||||||
return fileName.substring(fileName.lastIndexOf(".") + 1)
|
|
||||||
.lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTreeUriValid(uri: Uri): Boolean {
|
|
||||||
val resolver = context.contentResolver
|
|
||||||
val columns = arrayOf(
|
|
||||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
|
||||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
|
||||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
|
||||||
)
|
|
||||||
return try {
|
|
||||||
val docId: String = if (isRootTreeUri(uri)) {
|
|
||||||
DocumentsContract.getTreeDocumentId(uri)
|
|
||||||
} else {
|
|
||||||
DocumentsContract.getDocumentId(uri)
|
|
||||||
}
|
|
||||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
|
|
||||||
resolver.query(childrenUri, columns, null, null, null)
|
|
||||||
true
|
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInputStream(path: String) = if (path.contains("content://")) {
|
|
||||||
Uri.parse(path).inputStream()
|
|
||||||
} else {
|
|
||||||
File(path).inputStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOutputStream(path: String) = if (path.contains("content://")) {
|
|
||||||
Uri.parse(path).outputStream()
|
|
||||||
} else {
|
|
||||||
File(path).outputStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getStringFromFile(file: File): String =
|
|
||||||
String(file.readBytes(), StandardCharsets.UTF_8)
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getStringFromInputStream(stream: InputStream): String =
|
|
||||||
String(stream.readBytes(), StandardCharsets.UTF_8)
|
|
||||||
|
|
||||||
fun DocumentFile.inputStream(): InputStream =
|
|
||||||
SudachiApplication.appContext.contentResolver.openInputStream(uri)!!
|
|
||||||
|
|
||||||
fun DocumentFile.outputStream(): OutputStream =
|
|
||||||
SudachiApplication.appContext.contentResolver.openOutputStream(uri)!!
|
|
||||||
|
|
||||||
fun Uri.inputStream(): InputStream =
|
|
||||||
SudachiApplication.appContext.contentResolver.openInputStream(this)!!
|
|
||||||
|
|
||||||
fun Uri.outputStream(): OutputStream =
|
|
||||||
SudachiApplication.appContext.contentResolver.openOutputStream(this)!!
|
|
||||||
|
|
||||||
fun Uri.asDocumentFile(): DocumentFile? =
|
|
||||||
DocumentFile.fromSingleUri(SudachiApplication.appContext, this)
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.model.Game
|
|
||||||
import org.sudachi.sudachi_emu.model.GameDir
|
|
||||||
import org.sudachi.sudachi_emu.model.MinimalDocumentFile
|
|
||||||
|
|
||||||
object GameHelper {
|
|
||||||
private const val KEY_OLD_GAME_PATH = "game_path"
|
|
||||||
const val KEY_GAMES = "Games"
|
|
||||||
|
|
||||||
private lateinit var preferences: SharedPreferences
|
|
||||||
|
|
||||||
fun getGames(): List<Game> {
|
|
||||||
val games = mutableListOf<Game>()
|
|
||||||
val context = SudachiApplication.appContext
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
val gameDirs = mutableListOf<GameDir>()
|
|
||||||
val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
|
|
||||||
if (oldGamesDir.isNotEmpty()) {
|
|
||||||
gameDirs.add(GameDir(oldGamesDir, true))
|
|
||||||
preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
|
|
||||||
}
|
|
||||||
gameDirs.addAll(NativeConfig.getGameDirs())
|
|
||||||
|
|
||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
|
||||||
NativeLibrary.reloadKeys()
|
|
||||||
|
|
||||||
// Reset metadata so we don't use stale information
|
|
||||||
GameMetadata.resetMetadata()
|
|
||||||
|
|
||||||
// Remove previous filesystem provider information so we can get up to date version info
|
|
||||||
NativeLibrary.clearFilesystemProvider()
|
|
||||||
|
|
||||||
val badDirs = mutableListOf<Int>()
|
|
||||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
|
||||||
val gameDirUri = Uri.parse(gameDir.uriString)
|
|
||||||
val isValid = FileUtil.isTreeUriValid(gameDirUri)
|
|
||||||
if (isValid) {
|
|
||||||
addGamesRecursive(
|
|
||||||
games,
|
|
||||||
FileUtil.listFiles(gameDirUri),
|
|
||||||
if (gameDir.deepScan) 3 else 1
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
badDirs.add(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all game dirs with insufficient permissions from config
|
|
||||||
if (badDirs.isNotEmpty()) {
|
|
||||||
var offset = 0
|
|
||||||
badDirs.forEach {
|
|
||||||
gameDirs.removeAt(it - offset)
|
|
||||||
offset++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
|
||||||
|
|
||||||
// Cache list of games found on disk
|
|
||||||
val serializedGames = mutableSetOf<String>()
|
|
||||||
games.forEach {
|
|
||||||
serializedGames.add(Json.encodeToString(it))
|
|
||||||
}
|
|
||||||
preferences.edit()
|
|
||||||
.remove(KEY_GAMES)
|
|
||||||
.putStringSet(KEY_GAMES, serializedGames)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
return games.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addGamesRecursive(
|
|
||||||
games: MutableList<Game>,
|
|
||||||
files: Array<MinimalDocumentFile>,
|
|
||||||
depth: Int
|
|
||||||
) {
|
|
||||||
if (depth <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files.forEach {
|
|
||||||
if (it.isDirectory) {
|
|
||||||
addGamesRecursive(
|
|
||||||
games,
|
|
||||||
FileUtil.listFiles(it.uri),
|
|
||||||
depth - 1
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
|
|
||||||
val game = getGame(it.uri, true)
|
|
||||||
if (game != null) {
|
|
||||||
games.add(game)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGame(uri: Uri, addedToLibrary: Boolean): Game? {
|
|
||||||
val filePath = uri.toString()
|
|
||||||
if (!GameMetadata.getIsValid(filePath)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Needed to update installed content information
|
|
||||||
NativeLibrary.addFileToFilesystemProvider(filePath)
|
|
||||||
|
|
||||||
var name = GameMetadata.getTitle(filePath)
|
|
||||||
|
|
||||||
// If the game's title field is empty, use the filename.
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
name = FileUtil.getFilename(uri)
|
|
||||||
}
|
|
||||||
var programId = GameMetadata.getProgramId(filePath)
|
|
||||||
|
|
||||||
// If the game's ID field is empty, use the filename without extension.
|
|
||||||
if (programId.isEmpty()) {
|
|
||||||
programId = name.substring(0, name.lastIndexOf("."))
|
|
||||||
}
|
|
||||||
|
|
||||||
val newGame = Game(
|
|
||||||
name,
|
|
||||||
filePath,
|
|
||||||
programId,
|
|
||||||
GameMetadata.getDeveloper(filePath),
|
|
||||||
GameMetadata.getVersion(filePath, false),
|
|
||||||
GameMetadata.getIsHomebrew(filePath)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (addedToLibrary) {
|
|
||||||
val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
|
|
||||||
if (addedTime == 0L) {
|
|
||||||
preferences.edit()
|
|
||||||
.putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newGame
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,229 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.Surface
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import org.sudachi.sudachi_emu.NativeLibrary
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import org.sudachi.sudachi_emu.features.settings.model.StringSetting
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.util.zip.ZipException
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
|
|
||||||
object GpuDriverHelper {
|
|
||||||
private const val META_JSON_FILENAME = "meta.json"
|
|
||||||
private var fileRedirectionPath: String? = null
|
|
||||||
var driverInstallationPath: String? = null
|
|
||||||
private var hookLibPath: String? = null
|
|
||||||
|
|
||||||
val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/"
|
|
||||||
|
|
||||||
fun initializeDriverParameters() {
|
|
||||||
try {
|
|
||||||
// Initialize the file redirection directory.
|
|
||||||
fileRedirectionPath = SudachiApplication.appContext
|
|
||||||
.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
|
|
||||||
|
|
||||||
// Initialize the driver installation directory.
|
|
||||||
driverInstallationPath = SudachiApplication.appContext
|
|
||||||
.filesDir.canonicalPath + "/gpu_driver/"
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize directories.
|
|
||||||
initializeDirectories()
|
|
||||||
|
|
||||||
// Initialize hook libraries directory.
|
|
||||||
hookLibPath = SudachiApplication.appContext.applicationInfo.nativeLibraryDir + "/"
|
|
||||||
|
|
||||||
// Initialize GPU driver.
|
|
||||||
NativeLibrary.initializeGpuDriver(
|
|
||||||
hookLibPath,
|
|
||||||
driverInstallationPath,
|
|
||||||
installedCustomDriverData.libraryName,
|
|
||||||
fileRedirectionPath
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> {
|
|
||||||
val driverZips = File(driverStoragePath).listFiles()
|
|
||||||
val drivers: MutableList<Pair<String, GpuDriverMetadata>> =
|
|
||||||
driverZips
|
|
||||||
?.mapNotNull {
|
|
||||||
val metadata = getMetadataFromZip(it)
|
|
||||||
metadata.name?.let { _ -> Pair(it.path, metadata) }
|
|
||||||
}
|
|
||||||
?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
|
|
||||||
?.distinct()
|
|
||||||
?.toMutableList() ?: mutableListOf()
|
|
||||||
return drivers
|
|
||||||
}
|
|
||||||
|
|
||||||
fun installDefaultDriver() {
|
|
||||||
// Removing the installed driver will result in the backend using the default system driver.
|
|
||||||
File(driverInstallationPath!!).deleteRecursively()
|
|
||||||
initializeDriverParameters()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyDriverToInternalStorage(driverUri: Uri): Boolean {
|
|
||||||
// Ensure we have directories.
|
|
||||||
initializeDirectories()
|
|
||||||
|
|
||||||
// Copy the zip file URI to user data
|
|
||||||
val copiedFile =
|
|
||||||
FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
|
|
||||||
|
|
||||||
// Validate driver
|
|
||||||
val metadata = getMetadataFromZip(copiedFile)
|
|
||||||
if (metadata.name == null) {
|
|
||||||
copiedFile.delete()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.minApi > Build.VERSION.SDK_INT) {
|
|
||||||
copiedFile.delete()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies driver zip into user data directory so that it can be exported along with
|
|
||||||
* other user data and also unzipped into the installation directory
|
|
||||||
*/
|
|
||||||
fun installCustomDriver(driverUri: Uri): Boolean {
|
|
||||||
// Revert to system default in the event the specified driver is bad.
|
|
||||||
installDefaultDriver()
|
|
||||||
|
|
||||||
// Ensure we have directories.
|
|
||||||
initializeDirectories()
|
|
||||||
|
|
||||||
// Copy the zip file URI to user data
|
|
||||||
val copiedFile =
|
|
||||||
FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
|
|
||||||
|
|
||||||
// Validate driver
|
|
||||||
val metadata = getMetadataFromZip(copiedFile)
|
|
||||||
if (metadata.name == null) {
|
|
||||||
copiedFile.delete()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.minApi > Build.VERSION.SDK_INT) {
|
|
||||||
copiedFile.delete()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unzip the driver.
|
|
||||||
try {
|
|
||||||
FileUtil.unzipToInternalStorage(
|
|
||||||
copiedFile.path,
|
|
||||||
File(driverInstallationPath!!)
|
|
||||||
)
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the driver parameters.
|
|
||||||
initializeDriverParameters()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unzips driver into installation directory
|
|
||||||
*/
|
|
||||||
fun installCustomDriver(driver: File): Boolean {
|
|
||||||
// Revert to system default in the event the specified driver is bad.
|
|
||||||
installDefaultDriver()
|
|
||||||
|
|
||||||
// Ensure we have directories.
|
|
||||||
initializeDirectories()
|
|
||||||
|
|
||||||
// Validate driver
|
|
||||||
val metadata = getMetadataFromZip(driver)
|
|
||||||
if (metadata.name == null) {
|
|
||||||
driver.delete()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unzip the driver to the private installation directory
|
|
||||||
try {
|
|
||||||
FileUtil.unzipToInternalStorage(
|
|
||||||
driver.path,
|
|
||||||
File(driverInstallationPath!!)
|
|
||||||
)
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the driver parameters.
|
|
||||||
initializeDriverParameters()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes in a zip file and reads the meta.json file for presentation to the UI
|
|
||||||
*
|
|
||||||
* @param driver Zip containing driver and meta.json file
|
|
||||||
* @return A non-null [GpuDriverMetadata] instance that may have null members
|
|
||||||
*/
|
|
||||||
fun getMetadataFromZip(driver: File): GpuDriverMetadata {
|
|
||||||
try {
|
|
||||||
ZipFile(driver).use { zf ->
|
|
||||||
val entries = zf.entries()
|
|
||||||
while (entries.hasMoreElements()) {
|
|
||||||
val entry = entries.nextElement()
|
|
||||||
if (!entry.isDirectory && entry.name.lowercase().contains(".json")) {
|
|
||||||
zf.getInputStream(entry).use {
|
|
||||||
return GpuDriverMetadata(it, entry.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_: ZipException) {
|
|
||||||
} catch (_: FileNotFoundException) {
|
|
||||||
}
|
|
||||||
return GpuDriverMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun supportsCustomDriverLoading(): Boolean
|
|
||||||
|
|
||||||
external fun getSystemDriverInfo(
|
|
||||||
surface: Surface = Surface(SurfaceTexture(true)),
|
|
||||||
hookLibPath: String = GpuDriverHelper.hookLibPath!!
|
|
||||||
): Array<String>?
|
|
||||||
|
|
||||||
// Parse the custom driver metadata to retrieve the name.
|
|
||||||
val installedCustomDriverData: GpuDriverMetadata
|
|
||||||
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
|
|
||||||
|
|
||||||
val customDriverSettingData: GpuDriverMetadata
|
|
||||||
get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString()))
|
|
||||||
|
|
||||||
fun initializeDirectories() {
|
|
||||||
// Ensure the file redirection directory exists.
|
|
||||||
val fileRedirectionDir = File(fileRedirectionPath!!)
|
|
||||||
if (!fileRedirectionDir.exists()) {
|
|
||||||
fileRedirectionDir.mkdirs()
|
|
||||||
}
|
|
||||||
// Ensure the driver installation directory exists.
|
|
||||||
val driverInstallationDir = File(driverInstallationPath!!)
|
|
||||||
if (!driverInstallationDir.exists()) {
|
|
||||||
driverInstallationDir.mkdirs()
|
|
||||||
}
|
|
||||||
// Ensure the driver storage directory exists
|
|
||||||
val driverStorageDirectory = File(driverStoragePath)
|
|
||||||
if (!driverStorageDirectory.exists()) {
|
|
||||||
driverStorageDirectory.mkdirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.view.InputDevice
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import org.sudachi.sudachi_emu.features.input.NativeInput
|
|
||||||
import org.sudachi.sudachi_emu.features.input.SudachiInputOverlayDevice
|
|
||||||
import org.sudachi.sudachi_emu.features.input.SudachiPhysicalDevice
|
|
||||||
|
|
||||||
object InputHandler {
|
|
||||||
var androidControllers = mapOf<Int, SudachiPhysicalDevice>()
|
|
||||||
var registeredControllers = mutableListOf<ParamPackage>()
|
|
||||||
|
|
||||||
fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
|
||||||
val action = when (event.action) {
|
|
||||||
KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
|
|
||||||
KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var controllerData = androidControllers[event.device.controllerNumber]
|
|
||||||
if (controllerData == null) {
|
|
||||||
updateControllerData()
|
|
||||||
controllerData = androidControllers[event.device.controllerNumber] ?: return false
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeInput.onGamePadButtonEvent(
|
|
||||||
controllerData.getGUID(),
|
|
||||||
controllerData.getPort(),
|
|
||||||
event.keyCode,
|
|
||||||
action
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
|
|
||||||
val controllerData =
|
|
||||||
androidControllers[event.device.controllerNumber] ?: return false
|
|
||||||
event.device.motionRanges.forEach {
|
|
||||||
NativeInput.onGamePadAxisEvent(
|
|
||||||
controllerData.getGUID(),
|
|
||||||
controllerData.getPort(),
|
|
||||||
it.axis,
|
|
||||||
event.getAxisValue(it.axis)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDevices(): Map<Int, SudachiPhysicalDevice> {
|
|
||||||
val gameControllerDeviceIds = mutableMapOf<Int, SudachiPhysicalDevice>()
|
|
||||||
val deviceIds = InputDevice.getDeviceIds()
|
|
||||||
var port = 0
|
|
||||||
val inputSettings = NativeConfig.getInputSettings(true)
|
|
||||||
deviceIds.forEach { deviceId ->
|
|
||||||
InputDevice.getDevice(deviceId)?.apply {
|
|
||||||
// Verify that the device has gamepad buttons, control sticks, or both.
|
|
||||||
if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
|
|
||||||
sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
|
|
||||||
) {
|
|
||||||
if (!gameControllerDeviceIds.contains(controllerNumber)) {
|
|
||||||
gameControllerDeviceIds[controllerNumber] = SudachiPhysicalDevice(
|
|
||||||
this,
|
|
||||||
port,
|
|
||||||
inputSettings[port].useSystemVibrator
|
|
||||||
)
|
|
||||||
}
|
|
||||||
port++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gameControllerDeviceIds
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateControllerData() {
|
|
||||||
androidControllers = getDevices()
|
|
||||||
androidControllers.forEach {
|
|
||||||
NativeInput.registerController(it.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the input overlay on a dedicated port for all player 1 vibrations
|
|
||||||
NativeInput.registerController(SudachiInputOverlayDevice(androidControllers.isEmpty(), 100))
|
|
||||||
registeredControllers.clear()
|
|
||||||
NativeInput.getInputDevices().forEach {
|
|
||||||
registeredControllers.add(ParamPackage(it))
|
|
||||||
}
|
|
||||||
registeredControllers.sortBy { it.get("port", 0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId)
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects this [Flow] with a given [LifecycleOwner].
|
|
||||||
* @param scope [LifecycleOwner] that this [Flow] will be collected with.
|
|
||||||
* @param repeatState When to repeat collection on this [Flow].
|
|
||||||
* @param resetState Optional lambda to reset state of an underlying [MutableStateFlow] after
|
|
||||||
* [stateCollector] has been run.
|
|
||||||
* @param stateCollector Lambda that receives new state.
|
|
||||||
*/
|
|
||||||
inline fun <reified T> Flow<T>.collect(
|
|
||||||
scope: LifecycleOwner,
|
|
||||||
repeatState: Lifecycle.State = Lifecycle.State.CREATED,
|
|
||||||
crossinline resetState: () -> Unit = {},
|
|
||||||
crossinline stateCollector: (state: T) -> Unit
|
|
||||||
) {
|
|
||||||
scope.apply {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(repeatState) {
|
|
||||||
this@collect.collect {
|
|
||||||
stateCollector(it)
|
|
||||||
resetState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import org.sudachi.sudachi_emu.R
|
|
||||||
import org.sudachi.sudachi_emu.SudachiApplication
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
object MemoryUtil {
|
|
||||||
private val context get() = SudachiApplication.appContext
|
|
||||||
|
|
||||||
private val Float.hundredths: String
|
|
||||||
get() = String.format(Locale.ROOT, "%.2f", this)
|
|
||||||
|
|
||||||
// Required total system memory
|
|
||||||
const val REQUIRED_MEMORY = 8
|
|
||||||
|
|
||||||
const val Kb: Float = 1024F
|
|
||||||
const val Mb = Kb * 1024
|
|
||||||
const val Gb = Mb * 1024
|
|
||||||
const val Tb = Gb * 1024
|
|
||||||
const val Pb = Tb * 1024
|
|
||||||
const val Eb = Pb * 1024
|
|
||||||
|
|
||||||
fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
|
|
||||||
when {
|
|
||||||
size < Kb -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
size.hundredths,
|
|
||||||
context.getString(R.string.memory_byte_shorthand)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size < Mb -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
if (roundUp) ceil(size / Kb) else (size / Kb).hundredths,
|
|
||||||
context.getString(R.string.memory_kilobyte)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size < Gb -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
if (roundUp) ceil(size / Mb) else (size / Mb).hundredths,
|
|
||||||
context.getString(R.string.memory_megabyte)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size < Tb -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
if (roundUp) ceil(size / Gb) else (size / Gb).hundredths,
|
|
||||||
context.getString(R.string.memory_gigabyte)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size < Pb -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
if (roundUp) ceil(size / Tb) else (size / Tb).hundredths,
|
|
||||||
context.getString(R.string.memory_terabyte)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size < Eb -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
if (roundUp) ceil(size / Pb) else (size / Pb).hundredths,
|
|
||||||
context.getString(R.string.memory_petabyte)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
context.getString(
|
|
||||||
R.string.memory_formatted,
|
|
||||||
if (roundUp) ceil(size / Eb) else (size / Eb).hundredths,
|
|
||||||
context.getString(R.string.memory_exabyte)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalMemory: Float
|
|
||||||
get() {
|
|
||||||
val memInfo = ActivityManager.MemoryInfo()
|
|
||||||
with(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager) {
|
|
||||||
getMemoryInfo(memInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
memInfo.advertisedMem.toFloat()
|
|
||||||
} else {
|
|
||||||
memInfo.totalMem.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isLessThan(minimum: Int, size: Float): Boolean =
|
|
||||||
when (size) {
|
|
||||||
Kb -> totalMemory < Mb && totalMemory < minimum
|
|
||||||
Mb -> totalMemory < Gb && (totalMemory / Mb) < minimum
|
|
||||||
Gb -> totalMemory < Tb && (totalMemory / Gb) < minimum
|
|
||||||
Tb -> totalMemory < Pb && (totalMemory / Tb) < minimum
|
|
||||||
Pb -> totalMemory < Eb && (totalMemory / Pb) < minimum
|
|
||||||
Eb -> totalMemory / Eb < minimum
|
|
||||||
else -> totalMemory < Kb && totalMemory < minimum
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devices are unlikely to have 0.5GB increments of memory so we'll just round up to account for
|
|
||||||
// the potential error created by memInfo.totalMem
|
|
||||||
fun getDeviceRAM(): String = bytesToSizeUnit(totalMemory, true)
|
|
||||||
}
|
|
|
@ -1,186 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.model.GameDir
|
|
||||||
import org.sudachi.sudachi_emu.overlay.model.OverlayControlData
|
|
||||||
|
|
||||||
import org.sudachi.sudachi_emu.features.input.model.PlayerInput
|
|
||||||
|
|
||||||
object NativeConfig {
|
|
||||||
/**
|
|
||||||
* Loads global config.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun initializeGlobalConfig()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroys the stored global config object. This does not save the existing config.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun unloadGlobalConfig()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads values in the global config file and saves them.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun reloadGlobalConfig()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves global settings values in memory to disk.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun saveGlobalConfig()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates per-game config for the specified parameters. Must be unloaded once per-game config
|
|
||||||
* is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets
|
|
||||||
* will follow the per-game config until the global config is reloaded.
|
|
||||||
*
|
|
||||||
* @param programId String representation of the u64 programId
|
|
||||||
* @param fileName Filename of the game, including its extension
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun initializePerGameConfig(programId: String, fileName: String)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun isPerGameConfigLoaded(): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves per-game settings values in memory to disk.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun savePerGameConfig()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroys the stored per-game config object. This does not save the config.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun unloadPerGameConfig()
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getBoolean(key: String, needsGlobal: Boolean): Boolean
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setBoolean(key: String, value: Boolean)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getByte(key: String, needsGlobal: Boolean): Byte
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setByte(key: String, value: Byte)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getShort(key: String, needsGlobal: Boolean): Short
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setShort(key: String, value: Short)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getInt(key: String, needsGlobal: Boolean): Int
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setInt(key: String, value: Int)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getFloat(key: String, needsGlobal: Boolean): Float
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setFloat(key: String, value: Float)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getLong(key: String, needsGlobal: Boolean): Long
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setLong(key: String, value: Long)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getString(key: String, needsGlobal: Boolean): String
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setString(key: String, value: String)
|
|
||||||
|
|
||||||
external fun getIsRuntimeModifiable(key: String): Boolean
|
|
||||||
|
|
||||||
external fun getPairedSettingKey(key: String): String
|
|
||||||
|
|
||||||
external fun getIsSwitchable(key: String): Boolean
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun usingGlobal(key: String): Boolean
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setGlobal(key: String, global: Boolean)
|
|
||||||
|
|
||||||
external fun getIsSaveable(key: String): Boolean
|
|
||||||
|
|
||||||
external fun getDefaultToString(key: String): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets every [GameDir] in AndroidSettings::values.game_dirs
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun getGameDirs(): Array<GameDir>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun setGameDirs(dirs: Array<GameDir>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a single [GameDir] to the AndroidSettings::values.game_dirs array
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun addGameDir(dir: GameDir)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an array of the addons that are disabled for a given game
|
|
||||||
*
|
|
||||||
* @param programId String representation of a game's program ID
|
|
||||||
* @return An array of disabled addons
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun getDisabledAddons(programId: String): Array<String>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the disabled addons array corresponding to [programId] and replaces them
|
|
||||||
* with [disabledAddons]
|
|
||||||
*
|
|
||||||
* @param programId String representation of a game's program ID
|
|
||||||
* @param disabledAddons Replacement array of disabled addons
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun setDisabledAddons(programId: String, disabledAddons: Array<String>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an array of [OverlayControlData] from settings
|
|
||||||
*
|
|
||||||
* @return An array of [OverlayControlData]
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun getOverlayControlData(): Array<OverlayControlData>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the AndroidSettings::values.overlay_control_data array and replaces its values
|
|
||||||
* with [overlayControlData]
|
|
||||||
*
|
|
||||||
* @param overlayControlData Replacement array of [OverlayControlData]
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>)
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun getInputSettings(global: Boolean): Array<PlayerInput>
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
external fun setInputSettings(value: Array<PlayerInput>, global: Boolean)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves control values for a specific player
|
|
||||||
* Must be used when per game config is loaded
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
external fun saveControlPlayerValues()
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
// Kotlin version of src/common/param_package.h
|
|
||||||
class ParamPackage(serialized: String = "") {
|
|
||||||
private val KEY_VALUE_SEPARATOR = ":"
|
|
||||||
private val PARAM_SEPARATOR = ","
|
|
||||||
|
|
||||||
private val ESCAPE_CHARACTER = "$"
|
|
||||||
private val KEY_VALUE_SEPARATOR_ESCAPE = "$0"
|
|
||||||
private val PARAM_SEPARATOR_ESCAPE = "$1"
|
|
||||||
private val ESCAPE_CHARACTER_ESCAPE = "$2"
|
|
||||||
|
|
||||||
private val EMPTY_PLACEHOLDER = "[empty]"
|
|
||||||
|
|
||||||
val data = mutableMapOf<String, String>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val pairs = serialized.split(PARAM_SEPARATOR)
|
|
||||||
for (pair in pairs) {
|
|
||||||
val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList()
|
|
||||||
if (keyValue.size != 2) {
|
|
||||||
Log.error("[ParamPackage] Invalid key pair $keyValue")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
keyValue.forEachIndexed { i: Int, _: String ->
|
|
||||||
keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR)
|
|
||||||
keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR)
|
|
||||||
keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
set(keyValue[0], keyValue[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(params: List<Pair<String, String>>) : this() {
|
|
||||||
params.forEach {
|
|
||||||
data[it.first] = it.second
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun serialize(): String {
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
return EMPTY_PLACEHOLDER
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = StringBuilder()
|
|
||||||
data.forEach {
|
|
||||||
val keyValue = mutableListOf(it.key, it.value)
|
|
||||||
keyValue.forEachIndexed { i, _ ->
|
|
||||||
keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE)
|
|
||||||
keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE)
|
|
||||||
keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE)
|
|
||||||
}
|
|
||||||
result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR")
|
|
||||||
}
|
|
||||||
return result.removeSuffix(PARAM_SEPARATOR).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(key: String, defaultValue: String): String =
|
|
||||||
if (has(key)) {
|
|
||||||
data[key]!!
|
|
||||||
} else {
|
|
||||||
Log.debug("[ParamPackage] key $key not found")
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(key: String, defaultValue: Int): Int =
|
|
||||||
if (has(key)) {
|
|
||||||
try {
|
|
||||||
data[key]!!.toInt()
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int")
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.debug("[ParamPackage] key $key not found")
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Int.toBoolean(): Boolean =
|
|
||||||
if (this == 1) {
|
|
||||||
true
|
|
||||||
} else if (this == 0) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
throw Exception("Tried to convert a value to a boolean that was not 0 or 1!")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(key: String, defaultValue: Boolean): Boolean =
|
|
||||||
if (has(key)) {
|
|
||||||
try {
|
|
||||||
get(key, if (defaultValue) 1 else 0).toBoolean()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean")
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.debug("[ParamPackage] key $key not found")
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(key: String, defaultValue: Float): Float =
|
|
||||||
if (has(key)) {
|
|
||||||
try {
|
|
||||||
data[key]!!.toFloat()
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float")
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.debug("[ParamPackage] key $key not found")
|
|
||||||
defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
fun set(key: String, value: String) {
|
|
||||||
data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun set(key: String, value: Int) {
|
|
||||||
data[key] = value.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Boolean.toInt(): Int = if (this) 1 else 0
|
|
||||||
fun set(key: String, value: Boolean) {
|
|
||||||
data[key] = value.toInt().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun set(key: String, value: Float) {
|
|
||||||
data[key] = value.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun has(key: String): Boolean = data.containsKey(key)
|
|
||||||
|
|
||||||
fun erase(key: String) = data.remove(key)
|
|
||||||
|
|
||||||
fun clear() = data.clear()
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
|
|
||||||
object PreferenceUtil {
|
|
||||||
/**
|
|
||||||
* Retrieves a shared preference value and then deletes the value in storage.
|
|
||||||
* @param key Associated key for the value in this preferences instance
|
|
||||||
* @return Typed value associated with [key]. Null if no such key exists.
|
|
||||||
*/
|
|
||||||
inline fun <reified T> SharedPreferences.migratePreference(key: String): T? {
|
|
||||||
if (!this.contains(key)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val value: Any = when (T::class) {
|
|
||||||
String::class -> this.getString(key, "")!!
|
|
||||||
|
|
||||||
Boolean::class -> this.getBoolean(key, false)
|
|
||||||
|
|
||||||
Int::class -> this.getInt(key, 0)
|
|
||||||
|
|
||||||
Float::class -> this.getFloat(key, 0f)
|
|
||||||
|
|
||||||
Long::class -> this.getLong(key, 0)
|
|
||||||
|
|
||||||
else -> throw IllegalStateException("Tried to migrate preference with invalid type!")
|
|
||||||
}
|
|
||||||
deletePreference(key)
|
|
||||||
return value as T
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SharedPreferences.deletePreference(key: String) = this.edit().remove(key).apply()
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Parcelable
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
object SerializableHelper {
|
|
||||||
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
getSerializable(key, T::class.java)
|
|
||||||
} else {
|
|
||||||
getSerializable(key) as? T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Serializable> Intent.serializable(key: String): T? {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
getSerializableExtra(key, T::class.java)
|
|
||||||
} else {
|
|
||||||
getSerializableExtra(key) as? T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
getParcelable(key, T::class.java)
|
|
||||||
} else {
|
|
||||||
getParcelable(key) as? T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
getParcelableExtra(key, T::class.java)
|
|
||||||
} else {
|
|
||||||
getParcelableExtra(key) as? T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.utils
|
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
|
|
||||||
object ViewUtils {
|
|
||||||
fun showView(view: View, length: Long = 300) {
|
|
||||||
view.apply {
|
|
||||||
alpha = 0f
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
isClickable = true
|
|
||||||
}.animate().apply {
|
|
||||||
duration = length
|
|
||||||
alpha(1f)
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideView(view: View, length: Long = 300) {
|
|
||||||
if (view.visibility == View.INVISIBLE) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
view.apply {
|
|
||||||
alpha = 1f
|
|
||||||
isClickable = false
|
|
||||||
}.animate().apply {
|
|
||||||
duration = length
|
|
||||||
alpha(0f)
|
|
||||||
}.withEndAction {
|
|
||||||
view.visibility = View.INVISIBLE
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View.updateMargins(
|
|
||||||
left: Int = -1,
|
|
||||||
top: Int = -1,
|
|
||||||
right: Int = -1,
|
|
||||||
bottom: Int = -1
|
|
||||||
) {
|
|
||||||
val layoutParams = this.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
layoutParams.apply {
|
|
||||||
if (left != -1) {
|
|
||||||
leftMargin = left
|
|
||||||
}
|
|
||||||
if (top != -1) {
|
|
||||||
topMargin = top
|
|
||||||
}
|
|
||||||
if (right != -1) {
|
|
||||||
rightMargin = right
|
|
||||||
}
|
|
||||||
if (bottom != -1) {
|
|
||||||
bottomMargin = bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.layoutParams = layoutParams
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows or hides a view.
|
|
||||||
* @param visible Whether a view will be made View.VISIBLE or View.INVISIBLE/GONE.
|
|
||||||
* @param gone Optional parameter for hiding a view. Uses View.GONE if true and View.INVISIBLE otherwise.
|
|
||||||
*/
|
|
||||||
fun View.setVisible(visible: Boolean, gone: Boolean = true) {
|
|
||||||
visibility = if (visible) {
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
if (gone) {
|
|
||||||
View.GONE
|
|
||||||
} else {
|
|
||||||
View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a marquee on some text.
|
|
||||||
* @param delay Optional parameter for changing the start delay. 3 seconds of delay by default.
|
|
||||||
*/
|
|
||||||
fun TextView.marquee(delay: Long = 3000) {
|
|
||||||
ellipsize = null
|
|
||||||
marqueeRepeatLimit = -1
|
|
||||||
isSingleLine = true
|
|
||||||
postDelayed({
|
|
||||||
ellipsize = TextUtils.TruncateAt.MARQUEE
|
|
||||||
isSelected = true
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
package org.sudachi.sudachi_emu.views
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.util.Rational
|
|
||||||
import android.view.SurfaceView
|
|
||||||
|
|
||||||
class FixedRatioSurfaceView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : SurfaceView(context, attrs, defStyleAttr) {
|
|
||||||
private var aspectRatio: Float = 0f // (width / height), 0f is a special value for stretch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the desired aspect ratio for this view
|
|
||||||
* @param ratio the ratio to force the view to, or null to stretch to fit
|
|
||||||
*/
|
|
||||||
fun setAspectRatio(ratio: Rational?) {
|
|
||||||
aspectRatio = ratio?.toFloat() ?: 0f
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
||||||
val displayWidth: Float = MeasureSpec.getSize(widthMeasureSpec).toFloat()
|
|
||||||
val displayHeight: Float = MeasureSpec.getSize(heightMeasureSpec).toFloat()
|
|
||||||
if (aspectRatio != 0f) {
|
|
||||||
val displayAspect = displayWidth / displayHeight
|
|
||||||
if (displayAspect < aspectRatio) {
|
|
||||||
// Max out width
|
|
||||||
val halfHeight = displayHeight / 2
|
|
||||||
val surfaceHeight = displayWidth / aspectRatio
|
|
||||||
val newTop: Float = halfHeight - (surfaceHeight / 2)
|
|
||||||
val newBottom: Float = halfHeight + (surfaceHeight / 2)
|
|
||||||
super.onMeasure(
|
|
||||||
widthMeasureSpec,
|
|
||||||
MeasureSpec.makeMeasureSpec(
|
|
||||||
newBottom.toInt() - newTop.toInt(),
|
|
||||||
MeasureSpec.EXACTLY
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// Max out height
|
|
||||||
val halfWidth = displayWidth / 2
|
|
||||||
val surfaceWidth = displayHeight * aspectRatio
|
|
||||||
val newLeft: Float = halfWidth - (surfaceWidth / 2)
|
|
||||||
val newRight: Float = halfWidth + (surfaceWidth / 2)
|
|
||||||
super.onMeasure(
|
|
||||||
MeasureSpec.makeMeasureSpec(
|
|
||||||
newRight.toInt() - newLeft.toInt(),
|
|
||||||
MeasureSpec.EXACTLY
|
|
||||||
),
|
|
||||||
heightMeasureSpec
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
add_library(sudachi-android SHARED
|
|
||||||
emu_window/emu_window.cpp
|
|
||||||
emu_window/emu_window.h
|
|
||||||
native.cpp
|
|
||||||
native.h
|
|
||||||
native_config.cpp
|
|
||||||
android_settings.cpp
|
|
||||||
game_metadata.cpp
|
|
||||||
native_log.cpp
|
|
||||||
android_config.cpp
|
|
||||||
android_config.h
|
|
||||||
native_input.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
set_property(TARGET sudachi-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
|
||||||
|
|
||||||
target_link_libraries(sudachi-android PRIVATE audio_core common core input_common frontend_common Vulkan::Headers)
|
|
||||||
target_link_libraries(sudachi-android PRIVATE android camera2ndk EGL glad jnigraphics log)
|
|
||||||
if (ARCHITECTURE_arm64)
|
|
||||||
target_link_libraries(sudachi-android PRIVATE adrenotools)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} sudachi-android)
|
|
|
@ -1,60 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include "jni/android_common/android_common.h"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include "common/string_util.h"
|
|
||||||
#include "jni/id_cache.h"
|
|
||||||
|
|
||||||
std::string GetJString(JNIEnv* env, jstring jstr) {
|
|
||||||
if (!jstr) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const jchar* jchars = env->GetStringChars(jstr, nullptr);
|
|
||||||
const jsize length = env->GetStringLength(jstr);
|
|
||||||
const std::u16string_view string_view(reinterpret_cast<const char16_t*>(jchars), length);
|
|
||||||
const std::string converted_string = Common::UTF16ToUTF8(string_view);
|
|
||||||
env->ReleaseStringChars(jstr, jchars);
|
|
||||||
|
|
||||||
return converted_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring ToJString(JNIEnv* env, std::string_view str) {
|
|
||||||
const std::u16string converted_string = Common::UTF8ToUTF16(str);
|
|
||||||
return env->NewString(reinterpret_cast<const jchar*>(converted_string.data()),
|
|
||||||
static_cast<jint>(converted_string.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring ToJString(JNIEnv* env, std::u16string_view str) {
|
|
||||||
return ToJString(env, Common::UTF16ToUTF8(str));
|
|
||||||
}
|
|
||||||
|
|
||||||
double GetJDouble(JNIEnv* env, jobject jdouble) {
|
|
||||||
return env->GetDoubleField(jdouble, IDCache::GetDoubleValueField());
|
|
||||||
}
|
|
||||||
|
|
||||||
jobject ToJDouble(JNIEnv* env, double value) {
|
|
||||||
return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
s32 GetJInteger(JNIEnv* env, jobject jinteger) {
|
|
||||||
return env->GetIntField(jinteger, IDCache::GetIntegerValueField());
|
|
||||||
}
|
|
||||||
|
|
||||||
jobject ToJInteger(JNIEnv* env, s32 value) {
|
|
||||||
return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool GetJBoolean(JNIEnv* env, jobject jboolean) {
|
|
||||||
return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField());
|
|
||||||
}
|
|
||||||
|
|
||||||
jobject ToJBoolean(JNIEnv* env, bool value) {
|
|
||||||
return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
#include "common/common_types.h"
|
|
||||||
|
|
||||||
std::string GetJString(JNIEnv* env, jstring jstr);
|
|
||||||
jstring ToJString(JNIEnv* env, std::string_view str);
|
|
||||||
jstring ToJString(JNIEnv* env, std::u16string_view str);
|
|
||||||
|
|
||||||
double GetJDouble(JNIEnv* env, jobject jdouble);
|
|
||||||
jobject ToJDouble(JNIEnv* env, double value);
|
|
||||||
|
|
||||||
s32 GetJInteger(JNIEnv* env, jobject jinteger);
|
|
||||||
jobject ToJInteger(JNIEnv* env, s32 value);
|
|
||||||
|
|
||||||
bool GetJBoolean(JNIEnv* env, jobject jboolean);
|
|
||||||
jobject ToJBoolean(JNIEnv* env, bool value);
|
|
|
@ -1,337 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include <common/logging/log.h>
|
|
||||||
#include <input_common/main.h>
|
|
||||||
#include "android_config.h"
|
|
||||||
#include "android_settings.h"
|
|
||||||
#include "common/settings_setting.h"
|
|
||||||
|
|
||||||
AndroidConfig::AndroidConfig(const std::string& config_name, ConfigType config_type)
|
|
||||||
: Config(config_type) {
|
|
||||||
Initialize(config_name);
|
|
||||||
if (config_type != ConfigType::InputProfile) {
|
|
||||||
ReadAndroidValues();
|
|
||||||
SaveAndroidValues();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReloadAllValues() {
|
|
||||||
Reload();
|
|
||||||
ReadAndroidValues();
|
|
||||||
SaveAndroidValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveAllValues() {
|
|
||||||
SaveValues();
|
|
||||||
SaveAndroidValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadAndroidValues() {
|
|
||||||
if (global) {
|
|
||||||
ReadAndroidUIValues();
|
|
||||||
ReadUIValues();
|
|
||||||
ReadOverlayValues();
|
|
||||||
}
|
|
||||||
ReadDriverValues();
|
|
||||||
ReadAndroidControlValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadAndroidUIValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Android));
|
|
||||||
|
|
||||||
ReadCategory(Settings::Category::Android);
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadUIValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
|
||||||
|
|
||||||
ReadPathValues();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadPathValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
|
||||||
|
|
||||||
AndroidSettings::values.game_dirs.clear();
|
|
||||||
const int gamedirs_size = BeginArray(std::string("gamedirs"));
|
|
||||||
for (int i = 0; i < gamedirs_size; ++i) {
|
|
||||||
SetArrayIndex(i);
|
|
||||||
AndroidSettings::GameDir game_dir;
|
|
||||||
game_dir.path = ReadStringSetting(std::string("path"));
|
|
||||||
game_dir.deep_scan =
|
|
||||||
ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false));
|
|
||||||
AndroidSettings::values.game_dirs.push_back(game_dir);
|
|
||||||
}
|
|
||||||
EndArray();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadDriverValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
|
|
||||||
|
|
||||||
ReadCategory(Settings::Category::GpuDriver);
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadOverlayValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Overlay));
|
|
||||||
|
|
||||||
ReadCategory(Settings::Category::Overlay);
|
|
||||||
|
|
||||||
AndroidSettings::values.overlay_control_data.clear();
|
|
||||||
const int control_data_size = BeginArray("control_data");
|
|
||||||
for (int i = 0; i < control_data_size; ++i) {
|
|
||||||
SetArrayIndex(i);
|
|
||||||
AndroidSettings::OverlayControlData control_data;
|
|
||||||
control_data.id = ReadStringSetting(std::string("id"));
|
|
||||||
control_data.enabled = ReadBooleanSetting(std::string("enabled"));
|
|
||||||
control_data.landscape_position.first =
|
|
||||||
ReadDoubleSetting(std::string("landscape\\x_position"));
|
|
||||||
control_data.landscape_position.second =
|
|
||||||
ReadDoubleSetting(std::string("landscape\\y_position"));
|
|
||||||
control_data.portrait_position.first =
|
|
||||||
ReadDoubleSetting(std::string("portrait\\x_position"));
|
|
||||||
control_data.portrait_position.second =
|
|
||||||
ReadDoubleSetting(std::string("portrait\\y_position"));
|
|
||||||
control_data.foldable_position.first =
|
|
||||||
ReadDoubleSetting(std::string("foldable\\x_position"));
|
|
||||||
control_data.foldable_position.second =
|
|
||||||
ReadDoubleSetting(std::string("foldable\\y_position"));
|
|
||||||
AndroidSettings::values.overlay_control_data.push_back(control_data);
|
|
||||||
}
|
|
||||||
EndArray();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) {
|
|
||||||
std::string player_prefix;
|
|
||||||
if (type != ConfigType::InputProfile) {
|
|
||||||
player_prefix.append("player_").append(ToString(player_index)).append("_");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& player = Settings::values.players.GetValue()[player_index];
|
|
||||||
if (IsCustomConfig()) {
|
|
||||||
const auto profile_name =
|
|
||||||
ReadStringSetting(std::string(player_prefix).append("profile_name"));
|
|
||||||
if (profile_name.empty()) {
|
|
||||||
// Use the global input config
|
|
||||||
player = Settings::values.players.GetValue(true)[player_index];
|
|
||||||
player.profile_name = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android doesn't have default options for controllers. We have the input overlay for that.
|
|
||||||
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
|
|
||||||
const std::string default_param;
|
|
||||||
auto& player_buttons = player.buttons[i];
|
|
||||||
|
|
||||||
player_buttons = ReadStringSetting(
|
|
||||||
std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param);
|
|
||||||
if (player_buttons.empty()) {
|
|
||||||
player_buttons = default_param;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
|
|
||||||
const std::string default_param;
|
|
||||||
auto& player_analogs = player.analogs[i];
|
|
||||||
|
|
||||||
player_analogs = ReadStringSetting(
|
|
||||||
std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param);
|
|
||||||
if (player_analogs.empty()) {
|
|
||||||
player_analogs = default_param;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
|
|
||||||
const std::string default_param;
|
|
||||||
auto& player_motions = player.motions[i];
|
|
||||||
|
|
||||||
player_motions = ReadStringSetting(
|
|
||||||
std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param);
|
|
||||||
if (player_motions.empty()) {
|
|
||||||
player_motions = default_param;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
player.use_system_vibrator = ReadBooleanSetting(
|
|
||||||
std::string(player_prefix).append("use_system_vibrator"), player_index == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadAndroidControlValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
|
|
||||||
|
|
||||||
Settings::values.players.SetGlobal(!IsCustomConfig());
|
|
||||||
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
|
|
||||||
ReadAndroidPlayerValues(p);
|
|
||||||
}
|
|
||||||
if (IsCustomConfig()) {
|
|
||||||
EndGroup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ReadDebugControlValues();
|
|
||||||
// ReadHidbusValues();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveAndroidValues() {
|
|
||||||
if (global) {
|
|
||||||
SaveAndroidUIValues();
|
|
||||||
SaveUIValues();
|
|
||||||
SaveOverlayValues();
|
|
||||||
}
|
|
||||||
SaveDriverValues();
|
|
||||||
SaveAndroidControlValues();
|
|
||||||
|
|
||||||
WriteToIni();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveAndroidUIValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Android));
|
|
||||||
|
|
||||||
WriteCategory(Settings::Category::Android);
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveUIValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
|
||||||
|
|
||||||
SavePathValues();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SavePathValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
|
||||||
|
|
||||||
BeginArray(std::string("gamedirs"));
|
|
||||||
for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
|
|
||||||
SetArrayIndex(i);
|
|
||||||
const auto& game_dir = AndroidSettings::values.game_dirs[i];
|
|
||||||
WriteStringSetting(std::string("path"), game_dir.path);
|
|
||||||
WriteBooleanSetting(std::string("deep_scan"), game_dir.deep_scan,
|
|
||||||
std::make_optional(false));
|
|
||||||
}
|
|
||||||
EndArray();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveDriverValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
|
|
||||||
|
|
||||||
WriteCategory(Settings::Category::GpuDriver);
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveOverlayValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Overlay));
|
|
||||||
|
|
||||||
WriteCategory(Settings::Category::Overlay);
|
|
||||||
|
|
||||||
BeginArray("control_data");
|
|
||||||
for (size_t i = 0; i < AndroidSettings::values.overlay_control_data.size(); ++i) {
|
|
||||||
SetArrayIndex(i);
|
|
||||||
const auto& control_data = AndroidSettings::values.overlay_control_data[i];
|
|
||||||
WriteStringSetting(std::string("id"), control_data.id);
|
|
||||||
WriteBooleanSetting(std::string("enabled"), control_data.enabled);
|
|
||||||
WriteDoubleSetting(std::string("landscape\\x_position"),
|
|
||||||
control_data.landscape_position.first);
|
|
||||||
WriteDoubleSetting(std::string("landscape\\y_position"),
|
|
||||||
control_data.landscape_position.second);
|
|
||||||
WriteDoubleSetting(std::string("portrait\\x_position"),
|
|
||||||
control_data.portrait_position.first);
|
|
||||||
WriteDoubleSetting(std::string("portrait\\y_position"),
|
|
||||||
control_data.portrait_position.second);
|
|
||||||
WriteDoubleSetting(std::string("foldable\\x_position"),
|
|
||||||
control_data.foldable_position.first);
|
|
||||||
WriteDoubleSetting(std::string("foldable\\y_position"),
|
|
||||||
control_data.foldable_position.second);
|
|
||||||
}
|
|
||||||
EndArray();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) {
|
|
||||||
std::string player_prefix;
|
|
||||||
if (type != ConfigType::InputProfile) {
|
|
||||||
player_prefix = std::string("player_").append(ToString(player_index)).append("_");
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& player = Settings::values.players.GetValue()[player_index];
|
|
||||||
if (IsCustomConfig() && player.profile_name.empty()) {
|
|
||||||
// No custom profile selected
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string default_param;
|
|
||||||
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
|
|
||||||
WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]),
|
|
||||||
player.buttons[i], std::make_optional(default_param));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
|
|
||||||
WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]),
|
|
||||||
player.analogs[i], std::make_optional(default_param));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
|
|
||||||
WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]),
|
|
||||||
player.motions[i], std::make_optional(default_param));
|
|
||||||
}
|
|
||||||
WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"),
|
|
||||||
player.use_system_vibrator, std::make_optional(player_index == 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveAndroidControlValues() {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
|
|
||||||
|
|
||||||
Settings::values.players.SetGlobal(!IsCustomConfig());
|
|
||||||
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
|
|
||||||
SaveAndroidPlayerValues(p);
|
|
||||||
}
|
|
||||||
if (IsCustomConfig()) {
|
|
||||||
EndGroup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// SaveDebugControlValues();
|
|
||||||
// SaveHidbusValues();
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
|
||||||
auto& map = Settings::values.linkage.by_category;
|
|
||||||
if (map.contains(category)) {
|
|
||||||
return Settings::values.linkage.by_category[category];
|
|
||||||
}
|
|
||||||
return AndroidSettings::values.linkage.by_category[category];
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
|
|
||||||
|
|
||||||
ReadPlayerValues(player_index);
|
|
||||||
ReadAndroidPlayerValues(player_index);
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) {
|
|
||||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
|
|
||||||
|
|
||||||
LOG_DEBUG(Config, "Saving players control configuration values");
|
|
||||||
SavePlayerValues(player_index);
|
|
||||||
SaveAndroidPlayerValues(player_index);
|
|
||||||
|
|
||||||
EndGroup();
|
|
||||||
|
|
||||||
WriteToIni();
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 sudachi Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "frontend_common/config.h"
|
|
||||||
|
|
||||||
class AndroidConfig final : public Config {
|
|
||||||
public:
|
|
||||||
explicit AndroidConfig(const std::string& config_name = "config",
|
|
||||||
ConfigType config_type = ConfigType::GlobalConfig);
|
|
||||||
|
|
||||||
void ReloadAllValues() override;
|
|
||||||
void SaveAllValues() override;
|
|
||||||
|
|
||||||
void ReadAndroidControlPlayerValues(std::size_t player_index);
|
|
||||||
void SaveAndroidControlPlayerValues(std::size_t player_index);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void ReadAndroidPlayerValues(std::size_t player_index);
|
|
||||||
void ReadAndroidControlValues();
|
|
||||||
void ReadAndroidValues();
|
|
||||||
void ReadAndroidUIValues();
|
|
||||||
void ReadDriverValues();
|
|
||||||
void ReadOverlayValues();
|
|
||||||
void ReadHidbusValues() override {}
|
|
||||||
void ReadDebugControlValues() override {}
|
|
||||||
void ReadPathValues() override;
|
|
||||||
void ReadShortcutValues() override {}
|
|
||||||
void ReadUIValues() override;
|
|
||||||
void ReadUIGamelistValues() override {}
|
|
||||||
void ReadUILayoutValues() override {}
|
|
||||||
void ReadMultiplayerValues() override {}
|
|
||||||
|
|
||||||
void SaveAndroidPlayerValues(std::size_t player_index);
|
|
||||||
void SaveAndroidControlValues();
|
|
||||||
void SaveAndroidValues();
|
|
||||||
void SaveAndroidUIValues();
|
|
||||||
void SaveDriverValues();
|
|
||||||
void SaveOverlayValues();
|
|
||||||
void SaveHidbusValues() override {}
|
|
||||||
void SaveDebugControlValues() override {}
|
|
||||||
void SavePathValues() override;
|
|
||||||
void SaveShortcutValues() override {}
|
|
||||||
void SaveUIValues() override;
|
|
||||||
void SaveUIGamelistValues() override {}
|
|
||||||
void SaveUILayoutValues() override {}
|
|
||||||
void SaveMultiplayerValues() override {}
|
|
||||||
|
|
||||||
std::vector<Settings::BasicSetting*>& FindRelevantList(Settings::Category category) override;
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue