Merge branch 'master' of https://github.com/GPUCode/citra into vulkan-2
@@ -212,6 +212,10 @@ if (ENABLE_QT)
|
|||||||
|
|
||||||
find_package(Qt5 REQUIRED COMPONENTS Widgets Multimedia Concurrent ${QT_PREFIX_HINT})
|
find_package(Qt5 REQUIRED COMPONENTS Widgets Multimedia Concurrent ${QT_PREFIX_HINT})
|
||||||
|
|
||||||
|
if (UNIX AND NOT APPLE)
|
||||||
|
find_package(Qt5 REQUIRED COMPONENTS DBus ${QT_PREFIX_HINT})
|
||||||
|
endif()
|
||||||
|
|
||||||
if (ENABLE_QT_TRANSLATION)
|
if (ENABLE_QT_TRANSLATION)
|
||||||
find_package(Qt5 REQUIRED COMPONENTS LinguistTools ${QT_PREFIX_HINT})
|
find_package(Qt5 REQUIRED COMPONENTS LinguistTools ${QT_PREFIX_HINT})
|
||||||
endif()
|
endif()
|
||||||
@@ -387,10 +391,11 @@ endforeach()
|
|||||||
|
|
||||||
# Boost
|
# Boost
|
||||||
if (USE_SYSTEM_BOOST)
|
if (USE_SYSTEM_BOOST)
|
||||||
find_package(Boost 1.70.0 COMPONENTS serialization REQUIRED)
|
find_package(Boost 1.70.0 COMPONENTS serialization iostreams REQUIRED)
|
||||||
else()
|
else()
|
||||||
add_library(Boost::boost ALIAS boost)
|
add_library(Boost::boost ALIAS boost)
|
||||||
add_library(Boost::serialization ALIAS boost_serialization)
|
add_library(Boost::serialization ALIAS boost_serialization)
|
||||||
|
add_library(Boost::iostreams ALIAS boost_iostreams)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# SDL2
|
# SDL2
|
||||||
|
9
externals/CMakeLists.txt
vendored
@@ -21,6 +21,15 @@ file(GLOB boost_serialization_SRC "${CMAKE_SOURCE_DIR}/externals/boost/libs/seri
|
|||||||
add_library(boost_serialization STATIC ${boost_serialization_SRC})
|
add_library(boost_serialization STATIC ${boost_serialization_SRC})
|
||||||
target_link_libraries(boost_serialization PUBLIC boost)
|
target_link_libraries(boost_serialization PUBLIC boost)
|
||||||
|
|
||||||
|
# Boost::iostreams
|
||||||
|
add_library(
|
||||||
|
boost_iostreams
|
||||||
|
STATIC
|
||||||
|
${CMAKE_SOURCE_DIR}/externals/boost/libs/iostreams/src/file_descriptor.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/externals/boost/libs/iostreams/src/mapped_file.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(boost_iostreams PUBLIC boost)
|
||||||
|
|
||||||
# Add additional boost libs here; remember to ALIAS them in the root CMakeLists!
|
# Add additional boost libs here; remember to ALIAS them in the root CMakeLists!
|
||||||
|
|
||||||
# Catch2
|
# Catch2
|
||||||
|
2
externals/boost
vendored
@@ -23,7 +23,7 @@ android {
|
|||||||
// TODO If this is ever modified, change application_id in strings.xml
|
// TODO If this is ever modified, change application_id in strings.xml
|
||||||
applicationId "org.citra.citra_emu"
|
applicationId "org.citra.citra_emu"
|
||||||
minSdkVersion 28
|
minSdkVersion 28
|
||||||
targetSdkVersion 29
|
targetSdkVersion 31
|
||||||
versionCode autoVersion
|
versionCode autoVersion
|
||||||
versionName getVersion()
|
versionName getVersion()
|
||||||
ndk.abiFilters abiFilter
|
ndk.abiFilters abiFilter
|
||||||
@@ -102,7 +102,8 @@ android {
|
|||||||
arguments "-DENABLE_QT=0", // Don't use QT
|
arguments "-DENABLE_QT=0", // Don't use QT
|
||||||
"-DENABLE_SDL2=0", // Don't use SDL
|
"-DENABLE_SDL2=0", // Don't use SDL
|
||||||
"-DENABLE_WEB_SERVICE=0", // Don't use telemetry
|
"-DENABLE_WEB_SERVICE=0", // Don't use telemetry
|
||||||
"-DANDROID_ARM_NEON=true" // cryptopp requires Neon to work
|
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
|
||||||
|
"-DBUNDLE_SPEEX=ON"
|
||||||
|
|
||||||
abiFilters abiFilter
|
abiFilters abiFilter
|
||||||
}
|
}
|
||||||
@@ -111,22 +112,25 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation "androidx.activity:activity:1.5.1"
|
||||||
|
implementation "androidx.fragment:fragment:1.5.5"
|
||||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
|
implementation "androidx.documentfile:documentfile:1.0.1"
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
||||||
implementation 'androidx.fragment:fragment:1.5.3'
|
implementation 'androidx.fragment:fragment:1.5.3'
|
||||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||||
implementation 'com.google.android.material:material:1.6.1'
|
implementation 'com.google.android.material:material:1.6.1'
|
||||||
|
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||||
|
|
||||||
// For loading huge screenshots from the disk.
|
// For loading huge screenshots from the disk.
|
||||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||||
|
|
||||||
// Allows FRP-style asynchronous operations in Android.
|
// Allows FRP-style asynchronous operations in Android.
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||||
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
|
||||||
implementation 'org.ini4j:ini4j:0.5.4'
|
implementation 'org.ini4j:ini4j:0.5.4'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
@@ -134,6 +138,10 @@ dependencies {
|
|||||||
|
|
||||||
// Please don't upgrade the billing library as the newer version is not GPL-compatible
|
// Please don't upgrade the billing library as the newer version is not GPL-compatible
|
||||||
implementation 'com.android.billingclient:billing:2.0.3'
|
implementation 'com.android.billingclient:billing:2.0.3'
|
||||||
|
|
||||||
|
// To use the androidx.test.core APIs
|
||||||
|
androidTestImplementation "androidx.test:core:1.5.0"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:1.1.5"
|
||||||
}
|
}
|
||||||
|
|
||||||
def getVersion() {
|
def getVersion() {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package org.citra.citra_emu;
|
package org.citra.citra_emu;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.test.InstrumentationRegistry;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
@@ -19,7 +19,7 @@ public class ExampleInstrumentedTest {
|
|||||||
@Test
|
@Test
|
||||||
public void useAppContext() {
|
public void useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
Context appContext = ApplicationProvider.getApplicationContext();
|
||||||
|
|
||||||
assertEquals("org.citra.citra_emu", appContext.getPackageName());
|
assertEquals("org.citra.citra_emu", appContext.getPackageName());
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,6 @@
|
|||||||
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
|
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
|
||||||
<uses-feature android:name="android.hardware.opengles.aep" android:required="true" />
|
<uses-feature android:name="android.hardware.opengles.aep" android:required="true" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
@@ -44,7 +43,8 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
||||||
android:theme="@style/Theme.Citra.Main"
|
android:theme="@style/Theme.Citra.Splash.Main"
|
||||||
|
android:exported="true"
|
||||||
android:resizeableActivity="false">
|
android:resizeableActivity="false">
|
||||||
|
|
||||||
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
|
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
|
||||||
@@ -69,23 +69,12 @@
|
|||||||
|
|
||||||
<service android:name="org.citra.citra_emu.utils.ForegroundService"/>
|
<service android:name="org.citra.citra_emu.utils.ForegroundService"/>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="org.citra.citra_emu.activities.CustomFilePickerActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/FilePickerTheme">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.GET_CONTENT" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.Citra.Main"
|
android:theme="@style/Theme.Citra.Main"
|
||||||
android:label="@string/cheats"/>
|
android:label="@string/cheats"/>
|
||||||
|
|
||||||
<service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/>
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="org.citra.citra_emu.model.GameProvider"
|
android:name="org.citra.citra_emu.model.GameProvider"
|
||||||
@@ -93,16 +82,6 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.filesprovider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/nnf_provider_paths" />
|
|
||||||
</provider>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@@ -12,10 +12,12 @@ import android.os.Build;
|
|||||||
|
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
import org.citra.citra_emu.model.GameDatabase;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||||
|
import org.citra.citra_emu.utils.DocumentsTree;
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||||
|
|
||||||
public class CitraApplication extends Application {
|
public class CitraApplication extends Application {
|
||||||
public static GameDatabase databaseHelper;
|
public static GameDatabase databaseHelper;
|
||||||
|
public static DocumentsTree documentsTree;
|
||||||
private static CitraApplication application;
|
private static CitraApplication application;
|
||||||
|
|
||||||
private void createNotificationChannel() {
|
private void createNotificationChannel() {
|
||||||
@@ -39,6 +41,7 @@ public class CitraApplication extends Application {
|
|||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
application = this;
|
application = this;
|
||||||
|
documentsTree = new DocumentsTree();
|
||||||
|
|
||||||
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
|
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
|
||||||
DirectoryInitialization.start(getApplicationContext());
|
DirectoryInitialization.start(getApplicationContext());
|
||||||
|
@@ -28,6 +28,7 @@ import androidx.fragment.app.DialogFragment;
|
|||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
import org.citra.citra_emu.activities.EmulationActivity;
|
||||||
import org.citra.citra_emu.applets.SoftwareKeyboard;
|
import org.citra.citra_emu.applets.SoftwareKeyboard;
|
||||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||||
|
import org.citra.citra_emu.utils.FileUtil;
|
||||||
import org.citra.citra_emu.utils.Log;
|
import org.citra.citra_emu.utils.Log;
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||||
|
|
||||||
@@ -164,6 +165,10 @@ public final class NativeLibrary {
|
|||||||
// Create the config.ini file.
|
// Create the config.ini file.
|
||||||
public static native void CreateConfigFile();
|
public static native void CreateConfigFile();
|
||||||
|
|
||||||
|
public static native void CreateLogFile();
|
||||||
|
|
||||||
|
public static native void LogUserDirectory(String directory);
|
||||||
|
|
||||||
public static native int DefaultCPUCore();
|
public static native int DefaultCPUCore();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -262,11 +267,11 @@ public final class NativeLibrary {
|
|||||||
coreErrorAlertLock.notify();
|
coreErrorAlertLock.notify();
|
||||||
}
|
}
|
||||||
}).setOnDismissListener(dialog -> {
|
}).setOnDismissListener(dialog -> {
|
||||||
coreErrorAlertResult = true;
|
coreErrorAlertResult = true;
|
||||||
synchronized (coreErrorAlertLock) {
|
synchronized (coreErrorAlertLock) {
|
||||||
coreErrorAlertLock.notify();
|
coreErrorAlertLock.notify();
|
||||||
}
|
}
|
||||||
}).create();
|
}).create();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,4 +670,73 @@ public final class NativeLibrary {
|
|||||||
public static final int RELEASED = 0;
|
public static final int RELEASED = 0;
|
||||||
public static final int PRESSED = 1;
|
public static final int PRESSED = 1;
|
||||||
}
|
}
|
||||||
|
public static boolean createFile(String directory, String filename) {
|
||||||
|
if (FileUtil.isNativePath(directory)) {
|
||||||
|
return CitraApplication.documentsTree.createFile(directory, filename);
|
||||||
|
}
|
||||||
|
return FileUtil.createFile(CitraApplication.getAppContext(), directory, filename) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean createDir(String directory, String directoryName) {
|
||||||
|
if (FileUtil.isNativePath(directory)) {
|
||||||
|
return CitraApplication.documentsTree.createDir(directory, directoryName);
|
||||||
|
}
|
||||||
|
return FileUtil.createDir(CitraApplication.getAppContext(), directory, directoryName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int openContentUri(String path, String openMode) {
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
return CitraApplication.documentsTree.openContentUri(path, openMode);
|
||||||
|
}
|
||||||
|
return FileUtil.openContentUri(CitraApplication.getAppContext(), path, openMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] getFilesName(String path) {
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
return CitraApplication.documentsTree.getFilesName(path);
|
||||||
|
}
|
||||||
|
return FileUtil.getFilesName(CitraApplication.getAppContext(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getSize(String path) {
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
return CitraApplication.documentsTree.getFileSize(path);
|
||||||
|
}
|
||||||
|
return FileUtil.getFileSize(CitraApplication.getAppContext(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean fileExists(String path) {
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
return CitraApplication.documentsTree.Exists(path);
|
||||||
|
}
|
||||||
|
return FileUtil.Exists(CitraApplication.getAppContext(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDirectory(String path) {
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
return CitraApplication.documentsTree.isDirectory(path);
|
||||||
|
}
|
||||||
|
return FileUtil.isDirectory(CitraApplication.getAppContext(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||||
|
if (FileUtil.isNativePath(sourcePath) && FileUtil.isNativePath(destinationParentPath)) {
|
||||||
|
return CitraApplication.documentsTree.copyFile(sourcePath, destinationParentPath, destinationFilename);
|
||||||
|
}
|
||||||
|
return FileUtil.copyFile(CitraApplication.getAppContext(), sourcePath, destinationParentPath, destinationFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean renameFile(String path, String destinationFilename) {
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
return CitraApplication.documentsTree.renameFile(path, destinationFilename);
|
||||||
|
}
|
||||||
|
return FileUtil.renameFile(CitraApplication.getAppContext(), path, destinationFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean deleteDocument(String path) {
|
||||||
|
if (FileUtil.isNativePath(path)) {
|
||||||
|
return CitraApplication.documentsTree.deleteDocument(path);
|
||||||
|
}
|
||||||
|
return FileUtil.deleteDocument(CitraApplication.getAppContext(), path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,38 +0,0 @@
|
|||||||
package org.citra.citra_emu.activities;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
|
|
||||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.fragments.CustomFilePickerFragment;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
public class CustomFilePickerActivity extends FilePickerActivity {
|
|
||||||
public static final String EXTRA_TITLE = "filepicker.intent.TITLE";
|
|
||||||
public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected AbstractFilePickerFragment<File> getFragment(
|
|
||||||
@Nullable final String startPath, final int mode, final boolean allowMultiple,
|
|
||||||
final boolean allowCreateDir, final boolean allowExistingFile,
|
|
||||||
final boolean singleClick) {
|
|
||||||
CustomFilePickerFragment fragment = new CustomFilePickerFragment();
|
|
||||||
// startPath is allowed to be null. In that case, default folder should be SD-card and not "/"
|
|
||||||
fragment.setArgs(
|
|
||||||
startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(),
|
|
||||||
mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick);
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0);
|
|
||||||
fragment.setTitle(title);
|
|
||||||
String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS);
|
|
||||||
fragment.setAllowedExtensions(allowedExtensions);
|
|
||||||
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -5,8 +5,8 @@ import android.content.Intent;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.util.Pair;
|
||||||
import android.util.SparseIntArray;
|
import android.util.SparseIntArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
@@ -21,6 +21,8 @@ import android.widget.CheckBox;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.annotation.IntDef;
|
import androidx.annotation.IntDef;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -31,6 +33,7 @@ import androidx.fragment.app.FragmentActivity;
|
|||||||
import org.citra.citra_emu.CitraApplication;
|
import org.citra.citra_emu.CitraApplication;
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
||||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
||||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||||
@@ -85,6 +88,18 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
||||||
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
|
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<Boolean> mOpenFileLauncher =
|
||||||
|
registerForActivityResult(new OpenFileResultContract(), result -> {
|
||||||
|
if (result == null)
|
||||||
|
return;
|
||||||
|
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
|
||||||
|
result, getApplicationContext(), Collections.singletonList("bin"));
|
||||||
|
if (selectedFiles == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
onAmiiboSelected(selectedFiles[0]);
|
||||||
|
});
|
||||||
|
|
||||||
static {
|
static {
|
||||||
buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
|
buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
|
||||||
EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
|
EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
|
||||||
@@ -124,7 +139,6 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
.append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
|
.append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
private View mDecorView;
|
|
||||||
private EmulationFragment mEmulationFragment;
|
private EmulationFragment mEmulationFragment;
|
||||||
private SharedPreferences mPreferences;
|
private SharedPreferences mPreferences;
|
||||||
private ControllerMappingHelper mControllerMappingHelper;
|
private ControllerMappingHelper mControllerMappingHelper;
|
||||||
@@ -170,16 +184,6 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
mControllerMappingHelper = new ControllerMappingHelper();
|
mControllerMappingHelper = new ControllerMappingHelper();
|
||||||
|
|
||||||
// Get a handle to the Window containing the UI.
|
|
||||||
mDecorView = getWindow().getDecorView();
|
|
||||||
mDecorView.setOnSystemUiVisibilityChangeListener(visibility ->
|
|
||||||
{
|
|
||||||
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
|
||||||
// Go back to immersive fullscreen mode in 3s
|
|
||||||
Handler handler = new Handler(getMainLooper());
|
|
||||||
handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Set these options now so that the SurfaceView the game renders into is the right size.
|
// Set these options now so that the SurfaceView the game renders into is the right size.
|
||||||
enableFullscreenImmersive();
|
enableFullscreenImmersive();
|
||||||
|
|
||||||
@@ -275,14 +279,14 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void enableFullscreenImmersive() {
|
private void enableFullscreenImmersive() {
|
||||||
// It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
mDecorView.setSystemUiVisibility(
|
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE);
|
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -465,9 +469,7 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_ACTION_LOAD_AMIIBO:
|
case MENU_ACTION_LOAD_AMIIBO:
|
||||||
FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO,
|
mOpenFileLauncher.launch(false);
|
||||||
R.string.select_amiibo,
|
|
||||||
Collections.singletonList("bin"), false);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_ACTION_REMOVE_AMIIBO:
|
case MENU_ACTION_REMOVE_AMIIBO:
|
||||||
@@ -560,20 +562,8 @@ public final class EmulationActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
|
||||||
super.onActivityResult(requestCode, resultCode, result);
|
super.onActivityResult(requestCode, resultCode, result);
|
||||||
switch (requestCode) {
|
if (requestCode == StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER) {
|
||||||
case StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER:
|
StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
|
||||||
StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
|
|
||||||
break;
|
|
||||||
case REQUEST_SELECT_AMIIBO:
|
|
||||||
// If the user picked a file, as opposed to just backing out.
|
|
||||||
if (resultCode == MainActivity.RESULT_OK) {
|
|
||||||
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result);
|
|
||||||
if (selectedFiles == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
onAmiiboSelected(selectedFiles[0]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,15 +15,15 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import com.google.android.material.color.MaterialColors;
|
import com.google.android.material.color.MaterialColors;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.CitraApplication;
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
import org.citra.citra_emu.activities.EmulationActivity;
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
import org.citra.citra_emu.model.GameDatabase;
|
||||||
|
import org.citra.citra_emu.utils.FileUtil;
|
||||||
import org.citra.citra_emu.utils.Log;
|
import org.citra.citra_emu.utils.Log;
|
||||||
import org.citra.citra_emu.utils.PicassoUtils;
|
import org.citra.citra_emu.utils.PicassoUtils;
|
||||||
import org.citra.citra_emu.viewholders.GameViewHolder;
|
import org.citra.citra_emu.viewholders.GameViewHolder;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,8 +86,14 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
|
|||||||
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
|
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
|
||||||
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
||||||
|
|
||||||
final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
|
String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||||
holder.textFileName.setText(gamePath.getFileName().toString());
|
String filename;
|
||||||
|
if (FileUtil.isNativePath(filepath)) {
|
||||||
|
filename = CitraApplication.documentsTree.getFilename(filepath);
|
||||||
|
} else {
|
||||||
|
filename = FileUtil.getFilename(CitraApplication.getAppContext(), filepath);
|
||||||
|
}
|
||||||
|
holder.textFileName.setText(filename);
|
||||||
|
|
||||||
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
|
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
|
||||||
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
|
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
|
||||||
|
@@ -0,0 +1,24 @@
|
|||||||
|
package org.citra.citra_emu.contracts;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
public class OpenFileResultContract extends ActivityResultContract<Boolean, Intent> {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Intent createIntent(@NonNull Context context, Boolean allowMultiple) {
|
||||||
|
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
.setType("application/octet-stream")
|
||||||
|
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Intent parseResult(int i, @Nullable Intent intent) {
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,91 @@
|
|||||||
|
package org.citra.citra_emu.dialogs;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.utils.FileUtil;
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||||
|
|
||||||
|
public class CitraDirectoryDialog extends DialogFragment {
|
||||||
|
public static final String TAG = "citra_directory_dialog_fragment";
|
||||||
|
|
||||||
|
private static final String MOVE_DATE_ENABLE = "IS_MODE_DATA_ENABLE";
|
||||||
|
|
||||||
|
TextView pathView;
|
||||||
|
|
||||||
|
TextView spaceView;
|
||||||
|
|
||||||
|
CheckBox checkBox;
|
||||||
|
|
||||||
|
AlertDialog dialog;
|
||||||
|
|
||||||
|
Listener listener;
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onPressPositiveButton(boolean moveData, Uri path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CitraDirectoryDialog newInstance(String path, Listener listener) {
|
||||||
|
CitraDirectoryDialog frag = new CitraDirectoryDialog();
|
||||||
|
frag.listener = listener;
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putString("path", path);
|
||||||
|
frag.setArguments(args);
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
final FragmentActivity activity = requireActivity();
|
||||||
|
final Uri path = Uri.parse(Objects.requireNonNull(requireArguments().getString("path")));
|
||||||
|
SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
|
String freeSpaceText =
|
||||||
|
getResources().getString(R.string.free_space, FileUtil.getFreeSpace(activity, path));
|
||||||
|
|
||||||
|
LayoutInflater inflater = getLayoutInflater();
|
||||||
|
View view = inflater.inflate(R.layout.dialog_citra_directory, null);
|
||||||
|
|
||||||
|
checkBox = view.findViewById(R.id.checkBox);
|
||||||
|
pathView = view.findViewById(R.id.path);
|
||||||
|
spaceView = view.findViewById(R.id.space);
|
||||||
|
|
||||||
|
checkBox.setChecked(mPreferences.getBoolean(MOVE_DATE_ENABLE, true));
|
||||||
|
if (!PermissionsHandler.hasWriteAccess(activity)) {
|
||||||
|
checkBox.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
checkBox.setOnCheckedChangeListener(
|
||||||
|
(v, isChecked)
|
||||||
|
// record move data selection with SharedPreferences
|
||||||
|
-> mPreferences.edit().putBoolean(MOVE_DATE_ENABLE, checkBox.isChecked()).apply());
|
||||||
|
|
||||||
|
pathView.setText(path.getPath());
|
||||||
|
spaceView.setText(freeSpaceText);
|
||||||
|
|
||||||
|
setCancelable(false);
|
||||||
|
|
||||||
|
dialog = new MaterialAlertDialogBuilder(activity)
|
||||||
|
.setView(view)
|
||||||
|
.setIcon(R.mipmap.ic_launcher)
|
||||||
|
.setTitle(R.string.app_name)
|
||||||
|
.setPositiveButton(
|
||||||
|
android.R.string.ok,
|
||||||
|
(d, v) -> listener.onPressPositiveButton(checkBox.isChecked(), path))
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
package org.citra.citra_emu.dialogs;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
|
||||||
|
public class CopyDirProgressDialog extends DialogFragment {
|
||||||
|
public static final String TAG = "copy_dir_progress_dialog";
|
||||||
|
ProgressBar progressBar;
|
||||||
|
|
||||||
|
TextView progressText;
|
||||||
|
|
||||||
|
AlertDialog dialog;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
final FragmentActivity activity = requireActivity();
|
||||||
|
|
||||||
|
LayoutInflater inflater = getLayoutInflater();
|
||||||
|
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
||||||
|
|
||||||
|
progressBar = view.findViewById(R.id.progress_bar);
|
||||||
|
progressText = view.findViewById(R.id.progress_text);
|
||||||
|
progressText.setText("");
|
||||||
|
|
||||||
|
setCancelable(false);
|
||||||
|
|
||||||
|
dialog = new MaterialAlertDialogBuilder(activity)
|
||||||
|
.setView(view)
|
||||||
|
.setIcon(R.mipmap.ic_launcher)
|
||||||
|
.setTitle(R.string.move_data)
|
||||||
|
.setMessage("")
|
||||||
|
.create();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onUpdateSearchProgress(String msg) {
|
||||||
|
requireActivity().runOnUiThread(() -> {
|
||||||
|
dialog.setMessage(getResources().getString(R.string.searching_direcotry, msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onUpdateCopyProgress(String msg, int progress, int max) {
|
||||||
|
requireActivity().runOnUiThread(() -> {
|
||||||
|
progressBar.setProgress(progress);
|
||||||
|
progressBar.setMax(max);
|
||||||
|
progressText.setText(String.format("%d/%d", progress, max));
|
||||||
|
dialog.setMessage(getResources().getString(R.string.copy_file_name, msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -6,7 +6,6 @@ package org.citra.citra_emu.disk_shader_cache;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -56,10 +55,10 @@ public class DiskShaderCacheProgress {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
final Activity emulationActivity = Objects.requireNonNull(getActivity());
|
final Activity emulationActivity = requireActivity();
|
||||||
|
|
||||||
final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
|
final String title = Objects.requireNonNull(requireArguments().getString("title"));
|
||||||
final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
|
final String message = Objects.requireNonNull(requireArguments().getString("message"));
|
||||||
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(emulationActivity);
|
LayoutInflater inflater = LayoutInflater.from(emulationActivity);
|
||||||
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
||||||
@@ -75,15 +74,17 @@ public class DiskShaderCacheProgress {
|
|||||||
finishLock.notifyAll();
|
finishLock.notifyAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MaterialAlertDialogBuilder(emulationActivity)
|
dialog = new MaterialAlertDialogBuilder(emulationActivity)
|
||||||
|
.setView(view)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setMessage(message)
|
.setMessage(message)
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> emulationActivity.onBackPressed())
|
.setNegativeButton(android.R.string.cancel, (dialog, which) -> emulationActivity.onBackPressed())
|
||||||
.create();
|
.create();
|
||||||
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onUpdateProgress(String msg, int progress, int max) {
|
private void onUpdateProgress(String msg, int progress, int max) {
|
||||||
Objects.requireNonNull(getActivity()).runOnUiThread(() -> {
|
requireActivity().runOnUiThread(() -> {
|
||||||
progressBar.setProgress(progress);
|
progressBar.setProgress(progress);
|
||||||
progressBar.setMax(max);
|
progressBar.setMax(max);
|
||||||
progressText.setText(String.format("%d/%d", progress, max));
|
progressText.setText(String.format("%d/%d", progress, max));
|
||||||
|
@@ -11,7 +11,6 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
@@ -7,6 +7,9 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -19,6 +22,9 @@ import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
|||||||
import org.citra.citra_emu.ui.DividerItemDecoration;
|
import org.citra.citra_emu.ui.DividerItemDecoration;
|
||||||
|
|
||||||
public class CheatListFragment extends Fragment {
|
public class CheatListFragment extends Fragment {
|
||||||
|
private RecyclerView mRecyclerView;
|
||||||
|
private FloatingActionButton mFab;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
@@ -28,19 +34,38 @@ public class CheatListFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
RecyclerView recyclerView = view.findViewById(R.id.cheat_list);
|
mRecyclerView = view.findViewById(R.id.cheat_list);
|
||||||
FloatingActionButton fab = view.findViewById(R.id.fab);
|
mFab = view.findViewById(R.id.fab);
|
||||||
|
|
||||||
CheatsActivity activity = (CheatsActivity) requireActivity();
|
CheatsActivity activity = (CheatsActivity) requireActivity();
|
||||||
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
||||||
|
|
||||||
recyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
|
mRecyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
||||||
recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
|
mRecyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
|
||||||
|
|
||||||
fab.setOnClickListener(v -> {
|
mFab.setOnClickListener(v -> {
|
||||||
viewModel.startAddingCheat();
|
viewModel.startAddingCheat();
|
||||||
viewModel.openDetailsView();
|
viewModel.openDetailsView();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setInsets();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
||||||
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
v.setPadding(0, 0, 0, insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_fab_list));
|
||||||
|
|
||||||
|
ViewGroup.MarginLayoutParams mlpFab =
|
||||||
|
(ViewGroup.MarginLayoutParams) mFab.getLayoutParams();
|
||||||
|
int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large);
|
||||||
|
mlpFab.leftMargin = insets.left + fabPadding;
|
||||||
|
mlpFab.bottomMargin = insets.bottom + fabPadding;
|
||||||
|
mlpFab.rightMargin = insets.right + fabPadding;
|
||||||
|
mFab.setLayoutParams(mlpFab);
|
||||||
|
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,18 +10,26 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
||||||
|
|
||||||
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat;
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
|
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
|
||||||
|
import org.citra.citra_emu.utils.InsetsHelper;
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class CheatsActivity extends AppCompatActivity
|
public class CheatsActivity extends AppCompatActivity
|
||||||
implements SlidingPaneLayout.PanelSlideListener {
|
implements SlidingPaneLayout.PanelSlideListener {
|
||||||
private CheatsViewModel mViewModel;
|
private CheatsViewModel mViewModel;
|
||||||
@@ -44,14 +52,16 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
|
||||||
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
|
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
|
||||||
mViewModel.load();
|
mViewModel.load();
|
||||||
|
|
||||||
setContentView(R.layout.activity_cheats);
|
setContentView(R.layout.activity_cheats);
|
||||||
|
|
||||||
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
|
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
|
||||||
mCheatList = findViewById(R.id.cheat_list);
|
mCheatList = findViewById(R.id.cheat_list_container);
|
||||||
mCheatDetails = findViewById(R.id.cheat_details);
|
mCheatDetails = findViewById(R.id.cheat_details_container);
|
||||||
|
|
||||||
mCheatListLastFocus = mCheatList;
|
mCheatListLastFocus = mCheatList;
|
||||||
mCheatDetailsLastFocus = mCheatDetails;
|
mCheatDetailsLastFocus = mCheatDetails;
|
||||||
@@ -71,6 +81,8 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats);
|
MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
|
setInsets();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -153,8 +165,7 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setOnFocusChangeListenerRecursively(@NonNull View view,
|
public static void setOnFocusChangeListenerRecursively(@NonNull View view, View.OnFocusChangeListener listener) {
|
||||||
View.OnFocusChangeListener listener) {
|
|
||||||
view.setOnFocusChangeListener(listener);
|
view.setOnFocusChangeListener(listener);
|
||||||
|
|
||||||
if (view instanceof ViewGroup) {
|
if (view instanceof ViewGroup) {
|
||||||
@@ -165,4 +176,56 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
AppBarLayout appBarLayout = findViewById(R.id.appbar_cheats);
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(mSlidingPaneLayout, (v, windowInsets) -> {
|
||||||
|
Insets barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
Insets keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
|
||||||
|
|
||||||
|
InsetsHelper.insetAppBar(barInsets, appBarLayout);
|
||||||
|
mSlidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0);
|
||||||
|
|
||||||
|
// Set keyboard insets if the system supports smooth keyboard animations
|
||||||
|
ViewGroup.MarginLayoutParams mlpDetails =
|
||||||
|
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
|
||||||
|
if (keyboardInsets.bottom > 0) {
|
||||||
|
mlpDetails.bottomMargin = keyboardInsets.bottom;
|
||||||
|
} else {
|
||||||
|
mlpDetails.bottomMargin = barInsets.bottom;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mlpDetails.bottomMargin == 0) {
|
||||||
|
mlpDetails.bottomMargin = barInsets.bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mCheatDetails.setLayoutParams(mlpDetails);
|
||||||
|
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the layout for every frame that the keyboard animates in
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(mCheatDetails,
|
||||||
|
new WindowInsetsAnimationCompat.Callback(
|
||||||
|
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
|
||||||
|
int keyboardInsets = 0;
|
||||||
|
int barInsets = 0;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
|
||||||
|
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
|
||||||
|
ViewGroup.MarginLayoutParams mlpDetails =
|
||||||
|
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
|
||||||
|
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
|
||||||
|
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||||
|
mlpDetails.bottomMargin = Math.max(keyboardInsets, barInsets);
|
||||||
|
mCheatDetails.setLayoutParams(mlpDetails);
|
||||||
|
return insets;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,13 +8,19 @@ import android.os.Bundle;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
@@ -22,6 +28,7 @@ import org.citra.citra_emu.R;
|
|||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||||
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
||||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||||
|
import org.citra.citra_emu.utils.InsetsHelper;
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
|
|
||||||
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
|
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
|
||||||
@@ -44,9 +51,10 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
|||||||
ThemeUtil.applyTheme(this);
|
ThemeUtil.applyTheme(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
setContentView(R.layout.activity_settings);
|
setContentView(R.layout.activity_settings);
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
|
||||||
Intent launcher = getIntent();
|
Intent launcher = getIntent();
|
||||||
String gameID = launcher.getStringExtra(ARG_GAME_ID);
|
String gameID = launcher.getStringExtra(ARG_GAME_ID);
|
||||||
String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
|
String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
|
||||||
@@ -57,6 +65,8 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
|||||||
MaterialToolbar toolbar = findViewById(R.id.toolbar_settings);
|
MaterialToolbar toolbar = findViewById(R.id.toolbar_settings);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
|
setInsets();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -219,4 +229,14 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
|||||||
private SettingsFragment getFragment() {
|
private SettingsFragment getFragment() {
|
||||||
return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
|
return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
AppBarLayout appBar = findViewById(R.id.appbar_settings);
|
||||||
|
FrameLayout frame = findViewById(R.id.frame_content);
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
|
||||||
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
InsetsHelper.insetAppBar(insets, appBar);
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,9 @@ package org.citra.citra_emu.features.settings.ui;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
import java.io.File;
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
import org.citra.citra_emu.features.settings.model.Settings;
|
import org.citra.citra_emu.features.settings.model.Settings;
|
||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||||
@@ -15,8 +15,6 @@ import org.citra.citra_emu.utils.DirectoryStateReceiver;
|
|||||||
import org.citra.citra_emu.utils.Log;
|
import org.citra.citra_emu.utils.Log;
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
public final class SettingsActivityPresenter {
|
public final class SettingsActivityPresenter {
|
||||||
private static final String KEY_SHOULD_SAVE = "should_save";
|
private static final String KEY_SHOULD_SAVE = "should_save";
|
||||||
|
|
||||||
@@ -62,8 +60,8 @@ public final class SettingsActivityPresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void prepareCitraDirectoriesIfNeeded() {
|
private void prepareCitraDirectoriesIfNeeded() {
|
||||||
File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini");
|
DocumentFile configFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG);
|
||||||
if (!configFile.exists()) {
|
if (configFile == null || !configFile.exists()) {
|
||||||
Log.error("Citra config file could not be found!");
|
Log.error("Citra config file could not be found!");
|
||||||
}
|
}
|
||||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||||
|
@@ -8,6 +8,9 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
@@ -29,6 +32,8 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
|
|||||||
|
|
||||||
private SettingsAdapter mAdapter;
|
private SettingsAdapter mAdapter;
|
||||||
|
|
||||||
|
private RecyclerView mRecyclerView;
|
||||||
|
|
||||||
public static Fragment newInstance(String menuTag, String gameId) {
|
public static Fragment newInstance(String menuTag, String gameId) {
|
||||||
SettingsFragment fragment = new SettingsFragment();
|
SettingsFragment fragment = new SettingsFragment();
|
||||||
|
|
||||||
@@ -71,15 +76,17 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
|
|||||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||||
LinearLayoutManager manager = new LinearLayoutManager(getActivity());
|
LinearLayoutManager manager = new LinearLayoutManager(getActivity());
|
||||||
|
|
||||||
RecyclerView recyclerView = view.findViewById(R.id.list_settings);
|
mRecyclerView = view.findViewById(R.id.list_settings);
|
||||||
|
|
||||||
recyclerView.setAdapter(mAdapter);
|
mRecyclerView.setAdapter(mAdapter);
|
||||||
recyclerView.setLayoutManager(manager);
|
mRecyclerView.setLayoutManager(manager);
|
||||||
recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
|
mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
|
||||||
|
|
||||||
SettingsActivityView activity = (SettingsActivityView) getActivity();
|
SettingsActivityView activity = (SettingsActivityView) getActivity();
|
||||||
|
|
||||||
mPresenter.onViewCreated(activity.getSettings());
|
mPresenter.onViewCreated(activity.getSettings());
|
||||||
|
|
||||||
|
setInsets();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -133,4 +140,12 @@ public final class SettingsFragment extends Fragment implements SettingsFragment
|
|||||||
public void onSettingChanged() {
|
public void onSettingChanged() {
|
||||||
mActivity.onSettingChanged();
|
mActivity.onSettingChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
||||||
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
v.setPadding(insets.left, 0, insets.right, insets.bottom);
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,48 +0,0 @@
|
|||||||
package org.citra.citra_emu.features.settings.ui;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FrameLayout subclass with few Properties added to simplify animations.
|
|
||||||
* Don't remove the methods appearing as unused, in order not to break the menu animations
|
|
||||||
*/
|
|
||||||
public final class SettingsFrameLayout extends FrameLayout {
|
|
||||||
private float mVisibleness = 1.0f;
|
|
||||||
|
|
||||||
public SettingsFrameLayout(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsFrameLayout(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getYFraction() {
|
|
||||||
return getY() / getHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setYFraction(float yFraction) {
|
|
||||||
final int height = getHeight();
|
|
||||||
setY((height > 0) ? (yFraction * height) : -9999);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getVisibleness() {
|
|
||||||
return mVisibleness;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVisibleness(float visibleness) {
|
|
||||||
setScaleX(visibleness);
|
|
||||||
setScaleY(visibleness);
|
|
||||||
setAlpha(visibleness);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,10 @@
|
|||||||
package org.citra.citra_emu.features.settings.utils;
|
package org.citra.citra_emu.features.settings.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
import org.citra.citra_emu.CitraApplication;
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
@@ -18,10 +22,11 @@ import org.citra.citra_emu.utils.Log;
|
|||||||
import org.ini4j.Wini;
|
import org.ini4j.Wini;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
@@ -149,13 +154,15 @@ public final class SettingsFile {
|
|||||||
* @param view The current view.
|
* @param view The current view.
|
||||||
* @return An Observable that emits a HashMap of the file's contents, then completes.
|
* @return An Observable that emits a HashMap of the file's contents, then completes.
|
||||||
*/
|
*/
|
||||||
static HashMap<String, SettingSection> readFile(final File ini, boolean isCustomGame, SettingsActivityView view) {
|
static HashMap<String, SettingSection> readFile(final DocumentFile ini, boolean isCustomGame, SettingsActivityView view) {
|
||||||
HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
|
HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
|
||||||
|
|
||||||
BufferedReader reader = null;
|
BufferedReader reader = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
reader = new BufferedReader(new FileReader(ini));
|
Context context = CitraApplication.getAppContext();
|
||||||
|
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
||||||
|
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||||
|
|
||||||
SettingSection current = null;
|
SettingSection current = null;
|
||||||
for (String line; (line = reader.readLine()) != null; ) {
|
for (String line; (line = reader.readLine()) != null; ) {
|
||||||
@@ -170,11 +177,11 @@ public final class SettingsFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage());
|
Log.error("[SettingsFile] File not found: " + ini.getUri() + e.getMessage());
|
||||||
if (view != null)
|
if (view != null)
|
||||||
view.onSettingsFileNotFound();
|
view.onSettingsFileNotFound();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage());
|
Log.error("[SettingsFile] Error reading from: " + ini.getUri() + e.getMessage());
|
||||||
if (view != null)
|
if (view != null)
|
||||||
view.onSettingsFileNotFound();
|
view.onSettingsFileNotFound();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -182,7 +189,7 @@ public final class SettingsFile {
|
|||||||
try {
|
try {
|
||||||
reader.close();
|
reader.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.error("[SettingsFile] Error closing: " + ini.getAbsolutePath() + e.getMessage());
|
Log.error("[SettingsFile] Error closing: " + ini.getUri() + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,17 +223,23 @@ public final class SettingsFile {
|
|||||||
*/
|
*/
|
||||||
public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
|
public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
|
||||||
SettingsActivityView view) {
|
SettingsActivityView view) {
|
||||||
File ini = getSettingsFile(fileName);
|
DocumentFile ini = getSettingsFile(fileName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Wini writer = new Wini(ini);
|
Context context = CitraApplication.getAppContext();
|
||||||
|
InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
|
||||||
|
Wini writer = new Wini(inputStream);
|
||||||
|
|
||||||
Set<String> keySet = sections.keySet();
|
Set<String> keySet = sections.keySet();
|
||||||
for (String key : keySet) {
|
for (String key : keySet) {
|
||||||
SettingSection section = sections.get(key);
|
SettingSection section = sections.get(key);
|
||||||
writeSection(writer, section);
|
writeSection(writer, section);
|
||||||
}
|
}
|
||||||
writer.store();
|
inputStream.close();
|
||||||
|
OutputStream outputStream = context.getContentResolver().openOutputStream(ini.getUri());
|
||||||
|
writer.store(outputStream);
|
||||||
|
outputStream.flush();
|
||||||
|
outputStream.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
|
Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
|
||||||
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
|
view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
|
||||||
@@ -266,14 +279,16 @@ public final class SettingsFile {
|
|||||||
return generalSectionName;
|
return generalSectionName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
public static DocumentFile getSettingsFile(String fileName) {
|
||||||
private static File getSettingsFile(String fileName) {
|
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
|
||||||
return new File(
|
DocumentFile configDirectory = root.findFile("config");
|
||||||
DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini");
|
return configDirectory.findFile(fileName + ".ini");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File getCustomGameSettingsFile(String gameId) {
|
private static DocumentFile getCustomGameSettingsFile(String gameId) {
|
||||||
return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini");
|
DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.getAppContext(), Uri.parse(DirectoryInitialization.getUserDirectory()));
|
||||||
|
DocumentFile configDirectory = root.findFile("GameSettings");
|
||||||
|
return configDirectory.findFile(gameId + ".ini");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SettingSection sectionFromLine(String line, boolean isCustomGame) {
|
private static SettingSection sectionFromLine(String line, boolean isCustomGame) {
|
||||||
|
@@ -1,120 +0,0 @@
|
|||||||
package org.citra.citra_emu.fragments;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
|
|
||||||
import com.nononsenseapps.filepicker.FilePickerFragment;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class CustomFilePickerFragment extends FilePickerFragment {
|
|
||||||
private static String ALL_FILES = "*";
|
|
||||||
private int mTitle;
|
|
||||||
private static List<String> extensions = Collections.singletonList(ALL_FILES);
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Uri toUri(@NonNull final File file) {
|
|
||||||
return FileProvider
|
|
||||||
.getUriForFile(getContext(),
|
|
||||||
getContext().getApplicationContext().getPackageName() + ".filesprovider",
|
|
||||||
file);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(Bundle savedInstanceState) {
|
|
||||||
super.onActivityCreated(savedInstanceState);
|
|
||||||
|
|
||||||
if (mode == MODE_DIR) {
|
|
||||||
TextView ok = getActivity().findViewById(R.id.nnf_button_ok);
|
|
||||||
ok.setText(R.string.select_dir);
|
|
||||||
|
|
||||||
TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel);
|
|
||||||
cancel.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected View inflateRootView(LayoutInflater inflater, ViewGroup container) {
|
|
||||||
View view = super.inflateRootView(inflater, container);
|
|
||||||
if (mTitle != 0) {
|
|
||||||
Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar);
|
|
||||||
ViewGroup parent = (ViewGroup) toolbar.getParent();
|
|
||||||
int index = parent.indexOfChild(toolbar);
|
|
||||||
View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false);
|
|
||||||
TextView title = newToolbar.findViewById(R.id.filepicker_title);
|
|
||||||
title.setText(mTitle);
|
|
||||||
parent.removeView(toolbar);
|
|
||||||
parent.addView(newToolbar, index);
|
|
||||||
}
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(int title) {
|
|
||||||
mTitle = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAllowedExtensions(String allowedExtensions) {
|
|
||||||
if (allowedExtensions == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
extensions = Arrays.asList(allowedExtensions.split(","));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isItemVisible(@NonNull final File file) {
|
|
||||||
// Some users jump to the conclusion that Dolphin isn't able to detect their
|
|
||||||
// files if the files don't show up in the file picker when mode == MODE_DIR.
|
|
||||||
// To avoid this, show files even when the user needs to select a directory.
|
|
||||||
return (showHiddenItems || !file.isHidden()) &&
|
|
||||||
(file.isDirectory() || extensions.contains(ALL_FILES) ||
|
|
||||||
extensions.contains(fileExtension(file.getName()).toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCheckable(@NonNull final File file) {
|
|
||||||
// We need to make a small correction to the isCheckable logic due to
|
|
||||||
// overriding isItemVisible to show files when mode == MODE_DIR.
|
|
||||||
// AbstractFilePickerFragment always treats files as checkable when
|
|
||||||
// allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR.
|
|
||||||
return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void goUp() {
|
|
||||||
if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) {
|
|
||||||
goToDir(new File("/storage/"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mCurrentPath.equals(new File("/storage/"))){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
super.goUp();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) {
|
|
||||||
if(viewHolder.file.equals(new File("/storage/emulated/")))
|
|
||||||
viewHolder.file = new File("/storage/emulated/0/");
|
|
||||||
super.onClickDir(view, viewHolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String fileExtension(@NonNull String filename) {
|
|
||||||
int i = filename.lastIndexOf('.');
|
|
||||||
return i < 0 ? "" : filename.substring(i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,36 @@
|
|||||||
|
package org.citra.citra_emu.model;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A struct that is much more "cheaper" than DocumentFile.
|
||||||
|
* Only contains the information we needed.
|
||||||
|
*/
|
||||||
|
public class CheapDocument {
|
||||||
|
private final String filename;
|
||||||
|
private final Uri uri;
|
||||||
|
private final String mimeType;
|
||||||
|
|
||||||
|
public CheapDocument(String filename, String mimeType, Uri uri) {
|
||||||
|
this.filename = filename;
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFilename() {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri getUri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMimeType() {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirectory() {
|
||||||
|
return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||||
|
}
|
||||||
|
}
|
@@ -5,8 +5,10 @@ import android.content.Context;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
|
import org.citra.citra_emu.utils.FileUtil;
|
||||||
import org.citra.citra_emu.utils.Log;
|
import org.citra.citra_emu.utils.Log;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -64,10 +66,12 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
|
private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
|
||||||
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
|
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
public GameDatabase(Context context) {
|
public GameDatabase(Context context) {
|
||||||
// Superclass constructor builds a database or uses an existing one.
|
// Superclass constructor builds a database or uses an existing one.
|
||||||
super(context, "games.db", null, DB_VERSION);
|
super(context, "games.db", null, DB_VERSION);
|
||||||
|
mContext = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -151,9 +155,10 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
|||||||
while (folderCursor.moveToNext()) {
|
while (folderCursor.moveToNext()) {
|
||||||
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
|
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
|
||||||
|
|
||||||
File folder = new File(folderPath);
|
Uri folder = Uri.parse(folderPath);
|
||||||
// If the folder is empty because it no longer exists, remove it from the library.
|
// If the folder is empty because it no longer exists, remove it from the library.
|
||||||
if (!folder.exists()) {
|
CheapDocument[] files = FileUtil.listFiles(mContext, folder);
|
||||||
|
if (files.length == 0) {
|
||||||
Log.error(
|
Log.error(
|
||||||
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
|
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
|
||||||
database.delete(TABLE_NAME_FOLDERS,
|
database.delete(TABLE_NAME_FOLDERS,
|
||||||
@@ -161,7 +166,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
|||||||
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
|
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
|
||||||
}
|
}
|
||||||
|
|
||||||
addGamesRecursive(database, folder, allowedExtensions, 3);
|
addGamesRecursive(database, files, allowedExtensions, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
fileCursor.close();
|
fileCursor.close();
|
||||||
@@ -173,33 +178,28 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
|||||||
database.close();
|
database.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) {
|
private void addGamesRecursive(SQLiteDatabase database, CheapDocument[] files,
|
||||||
|
Set<String> allowedExtensions, int depth) {
|
||||||
if (depth <= 0) {
|
if (depth <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File[] children = parent.listFiles();
|
for (CheapDocument file : files) {
|
||||||
if (children != null) {
|
if (file.isDirectory()) {
|
||||||
for (File file : children) {
|
Set<String> newExtensions = new HashSet<>(Arrays.asList(
|
||||||
if (file.isHidden()) {
|
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app"));
|
||||||
continue;
|
CheapDocument[] children = FileUtil.listFiles(mContext, file.getUri());
|
||||||
}
|
this.addGamesRecursive(database, children, newExtensions, depth - 1);
|
||||||
|
} else {
|
||||||
|
String filename = file.getUri().toString();
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
int extensionStart = filename.lastIndexOf('.');
|
||||||
Set<String> newExtensions = new HashSet<>(Arrays.asList(
|
if (extensionStart > 0) {
|
||||||
".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app"));
|
String fileExtension = filename.substring(extensionStart);
|
||||||
addGamesRecursive(database, file, newExtensions, depth - 1);
|
|
||||||
} else {
|
|
||||||
String filePath = file.getPath();
|
|
||||||
|
|
||||||
int extensionStart = filePath.lastIndexOf('.');
|
// Check that the file has an extension we care about before trying to read out of it.
|
||||||
if (extensionStart > 0) {
|
if (allowedExtensions.contains(fileExtension.toLowerCase())) {
|
||||||
String fileExtension = filePath.substring(extensionStart);
|
attemptToAddGame(database, filename);
|
||||||
|
|
||||||
// Check that the file has an extension we care about before trying to read out of it.
|
|
||||||
if (allowedExtensions.contains(fileExtension.toLowerCase())) {
|
|
||||||
attemptToAddGame(database, filePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +1,46 @@
|
|||||||
package org.citra.citra_emu.ui.main;
|
package org.citra.citra_emu.ui.main;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import java.util.Collections;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
import org.citra.citra_emu.activities.EmulationActivity;
|
||||||
|
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
||||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||||
import org.citra.citra_emu.model.GameProvider;
|
import org.citra.citra_emu.model.GameProvider;
|
||||||
import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
|
import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
|
||||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
||||||
import org.citra.citra_emu.utils.BillingManager;
|
import org.citra.citra_emu.utils.BillingManager;
|
||||||
|
import org.citra.citra_emu.utils.CitraDirectoryHelper;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||||
|
import org.citra.citra_emu.utils.InsetsHelper;
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||||
import org.citra.citra_emu.utils.PicassoUtils;
|
import org.citra.citra_emu.utils.PicassoUtils;
|
||||||
import org.citra.citra_emu.utils.StartupHandler;
|
import org.citra.citra_emu.utils.StartupHandler;
|
||||||
import org.citra.citra_emu.utils.ThemeUtil;
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
|
* The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
|
||||||
* individually display a grid of available games for each Fragment, in a tabbed layout.
|
* individually display a grid of available games for each Fragment, in a tabbed layout.
|
||||||
@@ -46,13 +57,72 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||||||
|
|
||||||
private static MenuItem mPremiumButton;
|
private static MenuItem mPremiumButton;
|
||||||
|
|
||||||
|
private final CitraDirectoryHelper citraDirectoryHelper = new CitraDirectoryHelper(this, () -> {
|
||||||
|
// If mPlatformGamesFragment is null means game directory have not been set yet.
|
||||||
|
if (mPlatformGamesFragment == null) {
|
||||||
|
mPlatformGamesFragment = new PlatformGamesFragment();
|
||||||
|
getSupportFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.add(mFrameLayoutId, mPlatformGamesFragment)
|
||||||
|
.commit();
|
||||||
|
showGameInstallDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<Uri> mOpenCitraDirectory =
|
||||||
|
registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), result -> {
|
||||||
|
if (result == null)
|
||||||
|
return;
|
||||||
|
citraDirectoryHelper.showCitraDirectoryDialog(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<Uri> mOpenGameListLauncher =
|
||||||
|
registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), result -> {
|
||||||
|
if (result == null)
|
||||||
|
return;
|
||||||
|
int takeFlags =
|
||||||
|
(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
getContentResolver().takePersistableUriPermission(result, takeFlags);
|
||||||
|
// When a new directory is picked, we currently will reset the existing games
|
||||||
|
// database. This effectively means that only one game directory is supported.
|
||||||
|
// TODO(bunnei): Consider fixing this in the future, or removing code for this.
|
||||||
|
getContentResolver().insert(GameProvider.URI_RESET, null);
|
||||||
|
// Add the new directory
|
||||||
|
mPresenter.onDirectorySelected(result.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
private final ActivityResultLauncher<Boolean> mOpenFileLauncher =
|
||||||
|
registerForActivityResult(new OpenFileResultContract(), result -> {
|
||||||
|
if (result == null)
|
||||||
|
return;
|
||||||
|
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
|
||||||
|
result, getApplicationContext(), Collections.singletonList("cia"));
|
||||||
|
if (selectedFiles == null) {
|
||||||
|
Toast
|
||||||
|
.makeText(getApplicationContext(), R.string.cia_file_not_found,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NativeLibrary.InstallCIAS(selectedFiles);
|
||||||
|
mPresenter.refreshGameList();
|
||||||
|
});
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
|
||||||
|
splashScreen.setKeepOnScreenCondition(
|
||||||
|
()
|
||||||
|
-> (PermissionsHandler.hasWriteAccess(this) &&
|
||||||
|
!DirectoryInitialization.areCitraDirectoriesReady()));
|
||||||
|
|
||||||
ThemeUtil.applyTheme(this);
|
ThemeUtil.applyTheme(this);
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
|
||||||
findViews();
|
findViews();
|
||||||
|
|
||||||
setSupportActionBar(mToolbar);
|
setSupportActionBar(mToolbar);
|
||||||
@@ -61,7 +131,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||||||
mPresenter.onCreate();
|
mPresenter.onCreate();
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
StartupHandler.HandleInit(this);
|
StartupHandler.HandleInit(this, mOpenCitraDirectory);
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
mPlatformGamesFragment = new PlatformGamesFragment();
|
||||||
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
|
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
|
||||||
@@ -77,6 +147,8 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||||||
|
|
||||||
// Dismiss previous notifications (should not happen unless a crash occurred)
|
// Dismiss previous notifications (should not happen unless a crash occurred)
|
||||||
EmulationActivity.tryDismissRunningNotification(this);
|
EmulationActivity.tryDismissRunningNotification(this);
|
||||||
|
|
||||||
|
setInsets();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -144,7 +216,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||||
SettingsActivity.launch(this, menuTag, "");
|
SettingsActivity.launch(this, menuTag, "");
|
||||||
} else {
|
} else {
|
||||||
PermissionsHandler.checkWritePermission(this);
|
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,79 +224,18 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||||||
public void launchFileListActivity(int request) {
|
public void launchFileListActivity(int request) {
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
if (PermissionsHandler.hasWriteAccess(this)) {
|
||||||
switch (request) {
|
switch (request) {
|
||||||
|
case MainPresenter.REQUEST_SELECT_CITRA_DIRECTORY:
|
||||||
|
mOpenCitraDirectory.launch(null);
|
||||||
|
break;
|
||||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
||||||
FileBrowserHelper.openDirectoryPicker(this,
|
mOpenGameListLauncher.launch(null);
|
||||||
MainPresenter.REQUEST_ADD_DIRECTORY,
|
break;
|
||||||
R.string.select_game_folder,
|
|
||||||
Arrays.asList("elf", "axf", "cci", "3ds",
|
|
||||||
"cxi", "app", "3dsx", "cia",
|
|
||||||
"rar", "zip", "7z", "torrent",
|
|
||||||
"tar", "gz"));
|
|
||||||
break;
|
|
||||||
case MainPresenter.REQUEST_INSTALL_CIA:
|
case MainPresenter.REQUEST_INSTALL_CIA:
|
||||||
FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_INSTALL_CIA,
|
mOpenFileLauncher.launch(true);
|
||||||
R.string.install_cia_title,
|
break;
|
||||||
Collections.singletonList("cia"), true);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PermissionsHandler.checkWritePermission(this);
|
PermissionsHandler.checkWritePermission(this, mOpenCitraDirectory);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param requestCode An int describing whether the Activity that is returning did so successfully.
|
|
||||||
* @param resultCode An int describing what Activity is giving us this callback.
|
|
||||||
* @param result The information the returning Activity is providing us.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, result);
|
|
||||||
switch (requestCode) {
|
|
||||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
|
||||||
// If the user picked a file, as opposed to just backing out.
|
|
||||||
if (resultCode == MainActivity.RESULT_OK) {
|
|
||||||
// When a new directory is picked, we currently will reset the existing games
|
|
||||||
// database. This effectively means that only one game directory is supported.
|
|
||||||
// TODO(bunnei): Consider fixing this in the future, or removing code for this.
|
|
||||||
getContentResolver().insert(GameProvider.URI_RESET, null);
|
|
||||||
// Add the new directory
|
|
||||||
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MainPresenter.REQUEST_INSTALL_CIA:
|
|
||||||
// If the user picked a file, as opposed to just backing out.
|
|
||||||
if (resultCode == MainActivity.RESULT_OK) {
|
|
||||||
NativeLibrary.InstallCIAS(FileBrowserHelper.getSelectedFiles(result));
|
|
||||||
mPresenter.refeshGameList();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
switch (requestCode) {
|
|
||||||
case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
|
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
DirectoryInitialization.start(this);
|
|
||||||
|
|
||||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
|
||||||
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
|
|
||||||
.commit();
|
|
||||||
|
|
||||||
// Immediately prompt user to select a game directory on first boot
|
|
||||||
if (mPresenter != null) {
|
|
||||||
mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +256,18 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showGameInstallDialog() {
|
||||||
|
new MaterialAlertDialogBuilder(this)
|
||||||
|
.setIcon(R.mipmap.ic_launcher)
|
||||||
|
.setTitle(R.string.app_name)
|
||||||
|
.setMessage(R.string.app_game_install_description)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(android.R.string.ok,
|
||||||
|
(d, v) -> mOpenGameListLauncher.launch(null))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
EmulationActivity.tryDismissRunningNotification(this);
|
EmulationActivity.tryDismissRunningNotification(this);
|
||||||
@@ -266,4 +289,15 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||||||
public static void invokePremiumBilling(Runnable callback) {
|
public static void invokePremiumBilling(Runnable callback) {
|
||||||
mBillingManager.invokePremiumBilling(callback);
|
mBillingManager.invokePremiumBilling(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
AppBarLayout appBar = findViewById(R.id.appbar);
|
||||||
|
FrameLayout frame = findViewById(R.id.games_platform_frame);
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
|
||||||
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
InsetsHelper.insetAppBar(insets, appBar);
|
||||||
|
frame.setPadding(insets.left, 0, insets.right, 0);
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package org.citra.citra_emu.ui.main;
|
package org.citra.citra_emu.ui.main;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import org.citra.citra_emu.BuildConfig;
|
import org.citra.citra_emu.BuildConfig;
|
||||||
@@ -9,10 +10,12 @@ import org.citra.citra_emu.features.settings.model.Settings;
|
|||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||||
import org.citra.citra_emu.model.GameDatabase;
|
import org.citra.citra_emu.model.GameDatabase;
|
||||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
||||||
|
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||||
|
|
||||||
public final class MainPresenter {
|
public final class MainPresenter {
|
||||||
public static final int REQUEST_ADD_DIRECTORY = 1;
|
public static final int REQUEST_ADD_DIRECTORY = 1;
|
||||||
public static final int REQUEST_INSTALL_CIA = 2;
|
public static final int REQUEST_INSTALL_CIA = 2;
|
||||||
|
public static final int REQUEST_SELECT_CITRA_DIRECTORY = 3;
|
||||||
|
|
||||||
private final MainView mView;
|
private final MainView mView;
|
||||||
private String mDirToAdd;
|
private String mDirToAdd;
|
||||||
@@ -25,7 +28,7 @@ public final class MainPresenter {
|
|||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
String versionName = BuildConfig.VERSION_NAME;
|
String versionName = BuildConfig.VERSION_NAME;
|
||||||
mView.setVersionString(versionName);
|
mView.setVersionString(versionName);
|
||||||
refeshGameList();
|
refreshGameList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void launchFileListActivity(int request) {
|
public void launchFileListActivity(int request) {
|
||||||
@@ -46,6 +49,10 @@ public final class MainPresenter {
|
|||||||
mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG);
|
mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case R.id.button_select_root:
|
||||||
|
mView.launchFileListActivity(REQUEST_SELECT_CITRA_DIRECTORY);
|
||||||
|
return true;
|
||||||
|
|
||||||
case R.id.button_add_directory:
|
case R.id.button_add_directory:
|
||||||
launchFileListActivity(REQUEST_ADD_DIRECTORY);
|
launchFileListActivity(REQUEST_ADD_DIRECTORY);
|
||||||
return true;
|
return true;
|
||||||
@@ -74,9 +81,12 @@ public final class MainPresenter {
|
|||||||
mDirToAdd = dir;
|
mDirToAdd = dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refeshGameList() {
|
public void refreshGameList() {
|
||||||
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
Context context = CitraApplication.getAppContext();
|
||||||
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
if (PermissionsHandler.hasWriteAccess(context)) {
|
||||||
mView.refresh();
|
GameDatabase databaseHelper = CitraApplication.databaseHelper;
|
||||||
|
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
||||||
|
mView.refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,9 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -68,6 +70,8 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
|
|||||||
|
|
||||||
pullToRefresh.setProgressBackgroundColorSchemeColor(MaterialColors.getColor(pullToRefresh, R.attr.colorPrimary));
|
pullToRefresh.setProgressBackgroundColorSchemeColor(MaterialColors.getColor(pullToRefresh, R.attr.colorPrimary));
|
||||||
pullToRefresh.setColorSchemeColors(MaterialColors.getColor(pullToRefresh, R.attr.colorOnPrimary));
|
pullToRefresh.setColorSchemeColors(MaterialColors.getColor(pullToRefresh, R.attr.colorOnPrimary));
|
||||||
|
|
||||||
|
setInsets();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,4 +96,12 @@ public final class PlatformGamesFragment extends Fragment implements PlatformGam
|
|||||||
mRecyclerView = root.findViewById(R.id.grid_games);
|
mRecyclerView = root.findViewById(R.id.grid_games);
|
||||||
mTextView = root.findViewById(R.id.gamelist_empty_text);
|
mTextView = root.findViewById(R.id.gamelist_empty_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
||||||
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
v.setPadding(0, 0, 0, insets.bottom);
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,87 @@
|
|||||||
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import org.citra.citra_emu.dialogs.CitraDirectoryDialog;
|
||||||
|
import org.citra.citra_emu.dialogs.CopyDirProgressDialog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Citra directory initialization ui flow controller.
|
||||||
|
*/
|
||||||
|
public class CitraDirectoryHelper {
|
||||||
|
public interface Listener {
|
||||||
|
void onDirectoryInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final FragmentActivity mFragmentActivity;
|
||||||
|
private final Listener mListener;
|
||||||
|
|
||||||
|
public CitraDirectoryHelper(FragmentActivity mFragmentActivity, Listener mListener) {
|
||||||
|
this.mFragmentActivity = mFragmentActivity;
|
||||||
|
this.mListener = mListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showCitraDirectoryDialog(Uri result) {
|
||||||
|
CitraDirectoryDialog citraDirectoryDialog = CitraDirectoryDialog.newInstance(
|
||||||
|
result.toString(), ((moveData, path) -> {
|
||||||
|
Uri previous = PermissionsHandler.getCitraDirectory();
|
||||||
|
// Do noting if user select the previous path.
|
||||||
|
if (path.equals(previous)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
mFragmentActivity.getContentResolver().takePersistableUriPermission(path,
|
||||||
|
takeFlags);
|
||||||
|
if (!moveData || previous == null) {
|
||||||
|
initializeCitraDirectory(path);
|
||||||
|
mListener.onDirectoryInitialized();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user check move data, show copy progress dialog.
|
||||||
|
showCopyDialog(previous, path);
|
||||||
|
}));
|
||||||
|
citraDirectoryDialog.show(mFragmentActivity.getSupportFragmentManager(),
|
||||||
|
CitraDirectoryDialog.TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCopyDialog(Uri previous, Uri path) {
|
||||||
|
CopyDirProgressDialog copyDirProgressDialog = new CopyDirProgressDialog();
|
||||||
|
copyDirProgressDialog.showNow(mFragmentActivity.getSupportFragmentManager(),
|
||||||
|
CopyDirProgressDialog.TAG);
|
||||||
|
|
||||||
|
// Run copy dir in background
|
||||||
|
Executors.newSingleThreadExecutor().execute(() -> {
|
||||||
|
FileUtil.copyDir(
|
||||||
|
mFragmentActivity, previous.toString(), path.toString(),
|
||||||
|
new FileUtil.CopyDirListener() {
|
||||||
|
@Override
|
||||||
|
public void onSearchProgress(String directoryName) {
|
||||||
|
copyDirProgressDialog.onUpdateSearchProgress(directoryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCopyProgress(String filename, int progress, int max) {
|
||||||
|
copyDirProgressDialog.onUpdateCopyProgress(filename, progress, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
initializeCitraDirectory(path);
|
||||||
|
copyDirProgressDialog.dismissAllowingStateLoss();
|
||||||
|
mListener.onDirectoryInitialized();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeCitraDirectory(Uri path) {
|
||||||
|
if (!PermissionsHandler.setCitraDirectory(path.toString()))
|
||||||
|
return;
|
||||||
|
DirectoryInitialization.resetCitraDirectoryState();
|
||||||
|
DirectoryInitialization.start(mFragmentActivity);
|
||||||
|
}
|
||||||
|
}
|
@@ -9,19 +9,18 @@ package org.citra.citra_emu.utils;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import org.citra.citra_emu.CitraApplication;
|
||||||
|
import org.citra.citra_emu.NativeLibrary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that spawns its own thread in order to copy several binary and shader files
|
* A service that spawns its own thread in order to copy several binary and shader files
|
||||||
@@ -49,6 +48,9 @@ public final class DirectoryInitialization {
|
|||||||
if (PermissionsHandler.hasWriteAccess(context)) {
|
if (PermissionsHandler.hasWriteAccess(context)) {
|
||||||
if (setCitraUserDirectory()) {
|
if (setCitraUserDirectory()) {
|
||||||
initializeInternalStorage(context);
|
initializeInternalStorage(context);
|
||||||
|
CitraApplication.documentsTree.setRoot(Uri.parse(userPath));
|
||||||
|
NativeLibrary.CreateLogFile();
|
||||||
|
NativeLibrary.LogUserDirectory(userPath);
|
||||||
NativeLibrary.CreateConfigFile();
|
NativeLibrary.CreateConfigFile();
|
||||||
directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
||||||
} else {
|
} else {
|
||||||
@@ -75,6 +77,11 @@ public final class DirectoryInitialization {
|
|||||||
return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void resetCitraDirectoryState() {
|
||||||
|
directoryState = null;
|
||||||
|
isCitraDirectoryInitializationRunning.compareAndSet(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
public static String getUserDirectory() {
|
public static String getUserDirectory() {
|
||||||
if (directoryState == null) {
|
if (directoryState == null) {
|
||||||
throw new IllegalStateException("DirectoryInitialization has to run at least once!");
|
throw new IllegalStateException("DirectoryInitialization has to run at least once!");
|
||||||
@@ -88,15 +95,11 @@ public final class DirectoryInitialization {
|
|||||||
private static native void SetSysDirectory(String path);
|
private static native void SetSysDirectory(String path);
|
||||||
|
|
||||||
private static boolean setCitraUserDirectory() {
|
private static boolean setCitraUserDirectory() {
|
||||||
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
|
Uri dataPath = PermissionsHandler.getCitraDirectory();
|
||||||
File externalPath = Environment.getExternalStorageDirectory();
|
if (dataPath != null) {
|
||||||
if (externalPath != null) {
|
userPath = dataPath.toString();
|
||||||
userPath = externalPath.getAbsolutePath() + "/citra-emu";
|
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
|
||||||
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
|
return true;
|
||||||
// NativeLibrary.SetUserDirectory(userPath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@@ -0,0 +1,271 @@
|
|||||||
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.CitraApplication;
|
||||||
|
import org.citra.citra_emu.model.CheapDocument;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cached document tree for citra user directory.
|
||||||
|
* For every filepath which is not startsWith "content://" will need to use this class to traverse.
|
||||||
|
* For example:
|
||||||
|
* C++ citra log file directory will be /log/citra_log.txt.
|
||||||
|
* After DocumentsTree.resolvePath() it will become content URI.
|
||||||
|
*/
|
||||||
|
public class DocumentsTree {
|
||||||
|
private DocumentsNode root;
|
||||||
|
private final Context context;
|
||||||
|
public static final String DELIMITER = "/";
|
||||||
|
|
||||||
|
public DocumentsTree() {
|
||||||
|
context = CitraApplication.getAppContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoot(Uri rootUri) {
|
||||||
|
root = null;
|
||||||
|
root = new DocumentsNode();
|
||||||
|
root.uri = rootUri;
|
||||||
|
root.isDirectory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean createFile(String filepath, String name) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null) return false;
|
||||||
|
if (!node.isDirectory) return false;
|
||||||
|
if (!node.loaded) structTree(node);
|
||||||
|
Uri mUri = node.uri;
|
||||||
|
try {
|
||||||
|
String filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD);
|
||||||
|
if (node.children.get(filename) != null) return true;
|
||||||
|
DocumentFile createdFile = FileUtil.createFile(context, mUri.toString(), name);
|
||||||
|
if (createdFile == null) return false;
|
||||||
|
DocumentsNode document = new DocumentsNode(createdFile, false);
|
||||||
|
document.parent = node;
|
||||||
|
node.children.put(document.key, document);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[DocumentsTree]: Cannot create file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean createDir(String filepath, String name) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null) return false;
|
||||||
|
if (!node.isDirectory) return false;
|
||||||
|
if (!node.loaded) structTree(node);
|
||||||
|
Uri mUri = node.uri;
|
||||||
|
try {
|
||||||
|
String filename = URLDecoder.decode(name, FileUtil.DECODE_METHOD);
|
||||||
|
if (node.children.get(filename) != null) return true;
|
||||||
|
DocumentFile createdDirectory = FileUtil.createDir(context, mUri.toString(), name);
|
||||||
|
if (createdDirectory == null) return false;
|
||||||
|
DocumentsNode document = new DocumentsNode(createdDirectory, true);
|
||||||
|
document.parent = node;
|
||||||
|
node.children.put(document.key, document);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[DocumentsTree]: Cannot create file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int openContentUri(String filepath, String openmode) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return FileUtil.openContentUri(context, node.uri.toString(), openmode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFilename(String filepath) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return node.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getFilesName(String filepath) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null || !node.isDirectory) {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
// If this directory have not been iterate struct it.
|
||||||
|
if (!node.loaded) structTree(node);
|
||||||
|
return node.children.keySet().toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFileSize(String filepath) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null || node.isDirectory) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return FileUtil.getFileSize(context, node.uri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirectory(String filepath) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null) return false;
|
||||||
|
return node.isDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean Exists(String filepath) {
|
||||||
|
return resolvePath(filepath) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||||
|
DocumentsNode sourceNode = resolvePath(sourcePath);
|
||||||
|
if (sourceNode == null) return false;
|
||||||
|
DocumentsNode destinationNode = resolvePath(destinationParentPath);
|
||||||
|
if (destinationNode == null) return false;
|
||||||
|
try {
|
||||||
|
DocumentFile destinationParent = DocumentFile.fromTreeUri(context, destinationNode.uri);
|
||||||
|
if (destinationParent == null) return false;
|
||||||
|
String filename = URLDecoder.decode(destinationFilename, "UTF-8");
|
||||||
|
DocumentFile destination = destinationParent.createFile("application/octet-stream", filename);
|
||||||
|
if (destination == null) return false;
|
||||||
|
DocumentsNode document = new DocumentsNode();
|
||||||
|
document.uri = destination.getUri();
|
||||||
|
document.parent = destinationNode;
|
||||||
|
document.name = destination.getName();
|
||||||
|
document.isDirectory = destination.isDirectory();
|
||||||
|
document.loaded = true;
|
||||||
|
InputStream input = context.getContentResolver().openInputStream(sourceNode.uri);
|
||||||
|
OutputStream output = context.getContentResolver().openOutputStream(destination.getUri());
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = input.read(buffer)) != -1) {
|
||||||
|
output.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
input.close();
|
||||||
|
output.flush();
|
||||||
|
output.close();
|
||||||
|
destinationNode.children.put(document.key, document);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[DocumentsTree]: Cannot copy file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean renameFile(String filepath, String destinationFilename) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null) return false;
|
||||||
|
try {
|
||||||
|
Uri mUri = node.uri;
|
||||||
|
String filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD);
|
||||||
|
DocumentsContract.renameDocument(context.getContentResolver(), mUri, filename);
|
||||||
|
node.rename(filename);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[DocumentsTree]: Cannot rename file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteDocument(String filepath) {
|
||||||
|
DocumentsNode node = resolvePath(filepath);
|
||||||
|
if (node == null) return false;
|
||||||
|
try {
|
||||||
|
Uri mUri = node.uri;
|
||||||
|
if (!DocumentsContract.deleteDocument(context.getContentResolver(), mUri)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (node.parent != null) {
|
||||||
|
node.parent.children.remove(node.key);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[DocumentsTree]: Cannot rename file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private DocumentsNode resolvePath(String filepath) {
|
||||||
|
if (root == null)
|
||||||
|
return null;
|
||||||
|
StringTokenizer tokens = new StringTokenizer(filepath, DELIMITER, false);
|
||||||
|
DocumentsNode iterator = root;
|
||||||
|
while (tokens.hasMoreTokens()) {
|
||||||
|
String token = tokens.nextToken();
|
||||||
|
if (token.isEmpty()) continue;
|
||||||
|
iterator = find(iterator, token);
|
||||||
|
if (iterator == null) return null;
|
||||||
|
}
|
||||||
|
return iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private DocumentsNode find(DocumentsNode parent, String filename) {
|
||||||
|
if (parent.isDirectory && !parent.loaded) {
|
||||||
|
structTree(parent);
|
||||||
|
}
|
||||||
|
return parent.children.get(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct current level directory tree
|
||||||
|
*
|
||||||
|
* @param parent parent node of this level
|
||||||
|
*/
|
||||||
|
private void structTree(DocumentsNode parent) {
|
||||||
|
CheapDocument[] documents = FileUtil.listFiles(context, parent.uri);
|
||||||
|
for (CheapDocument document : documents) {
|
||||||
|
DocumentsNode node = new DocumentsNode(document);
|
||||||
|
node.parent = parent;
|
||||||
|
parent.children.put(node.key, node);
|
||||||
|
}
|
||||||
|
parent.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DocumentsNode {
|
||||||
|
private DocumentsNode parent;
|
||||||
|
private final Map<String, DocumentsNode> children = new HashMap<>();
|
||||||
|
private String key;
|
||||||
|
private String name;
|
||||||
|
private Uri uri;
|
||||||
|
private boolean loaded = false;
|
||||||
|
private boolean isDirectory = false;
|
||||||
|
|
||||||
|
private DocumentsNode() {}
|
||||||
|
|
||||||
|
private DocumentsNode(CheapDocument document) {
|
||||||
|
name = document.getFilename();
|
||||||
|
uri = document.getUri();
|
||||||
|
key = FileUtil.getFilenameWithExtensions(uri);
|
||||||
|
isDirectory = document.isDirectory();
|
||||||
|
loaded = !isDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DocumentsNode(DocumentFile document, boolean isCreateDir) {
|
||||||
|
name = document.getName();
|
||||||
|
uri = document.getUri();
|
||||||
|
key = FileUtil.getFilenameWithExtensions(uri);
|
||||||
|
isDirectory = isCreateDir;
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rename(String key) {
|
||||||
|
if (parent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent.children.remove(this.key);
|
||||||
|
this.name = key;
|
||||||
|
parent.children.put(key, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,71 +1,48 @@
|
|||||||
package org.citra.citra_emu.utils;
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import com.nononsenseapps.filepicker.FilePickerActivity;
|
import java.util.ArrayList;
|
||||||
import com.nononsenseapps.filepicker.Utils;
|
|
||||||
|
|
||||||
import org.citra.citra_emu.activities.CustomFilePickerActivity;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public final class FileBrowserHelper {
|
public final class FileBrowserHelper {
|
||||||
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
|
|
||||||
Intent i = new Intent(activity, CustomFilePickerActivity.class);
|
|
||||||
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
|
|
||||||
Environment.getExternalStorageDirectory().getPath());
|
|
||||||
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
|
|
||||||
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
|
|
||||||
|
|
||||||
activity.startActivityForResult(i, requestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void openFilePicker(FragmentActivity activity, int requestCode, int title,
|
|
||||||
List<String> extensions, boolean allowMultiple) {
|
|
||||||
Intent i = new Intent(activity, CustomFilePickerActivity.class);
|
|
||||||
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple);
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH,
|
|
||||||
Environment.getExternalStorageDirectory().getPath());
|
|
||||||
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
|
|
||||||
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
|
|
||||||
|
|
||||||
activity.startActivityForResult(i, requestCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static String getSelectedDirectory(Intent result) {
|
public static String[] getSelectedFiles(Intent result, Context context, List<String> extension) {
|
||||||
// Use the provided utility method to parse the result
|
ClipData clipData = result.getClipData();
|
||||||
List<Uri> files = Utils.getSelectedFilesFromResult(result);
|
List<DocumentFile> files = new ArrayList<>();
|
||||||
if (!files.isEmpty()) {
|
if (clipData == null) {
|
||||||
File file = Utils.getFileForUri(files.get(0));
|
files.add(DocumentFile.fromSingleUri(context, result.getData()));
|
||||||
return file.getAbsolutePath();
|
} else {
|
||||||
|
for (int i = 0; i < clipData.getItemCount(); i++) {
|
||||||
|
ClipData.Item item = clipData.getItemAt(i);
|
||||||
|
Uri uri = item.getUri();
|
||||||
|
files.add(DocumentFile.fromSingleUri(context, uri));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String[] getSelectedFiles(Intent result) {
|
|
||||||
// Use the provided utility method to parse the result
|
|
||||||
List<Uri> files = Utils.getSelectedFilesFromResult(result);
|
|
||||||
if (!files.isEmpty()) {
|
if (!files.isEmpty()) {
|
||||||
String[] paths = new String[files.size()];
|
List<String> filePaths = new ArrayList<>();
|
||||||
for (int i = 0; i < files.size(); i++)
|
for (int i = 0; i < files.size(); i++) {
|
||||||
paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
|
DocumentFile file = files.get(i);
|
||||||
return paths;
|
String filename = file.getName();
|
||||||
|
int extensionStart = filename.lastIndexOf('.');
|
||||||
|
if (extensionStart > 0) {
|
||||||
|
String fileExtension = filename.substring(extensionStart + 1);
|
||||||
|
if (extension.contains(fileExtension)) {
|
||||||
|
filePaths.add(file.getUri().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filePaths.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return filePaths.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@@ -1,11 +1,385 @@
|
|||||||
package org.citra.citra_emu.utils;
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.system.StructStatVfs;
|
||||||
|
import android.util.Pair;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.citra.citra_emu.model.CheapDocument;
|
||||||
|
|
||||||
public class FileUtil {
|
public class FileUtil {
|
||||||
|
static final String PATH_TREE = "tree";
|
||||||
|
static final String DECODE_METHOD = "UTF-8";
|
||||||
|
static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
|
||||||
|
static final String TEXT_PLAIN = "text/plain";
|
||||||
|
|
||||||
|
public interface CopyDirListener {
|
||||||
|
void onSearchProgress(String directoryName);
|
||||||
|
void onCopyProgress(String filename, int progress, int max);
|
||||||
|
|
||||||
|
void onComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a file from directory with filename.
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param directory parent path for file.
|
||||||
|
* @param filename file display name.
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static DocumentFile createFile(Context context, String directory, String filename) {
|
||||||
|
try {
|
||||||
|
Uri directoryUri = Uri.parse(directory);
|
||||||
|
DocumentFile parent;
|
||||||
|
parent = DocumentFile.fromTreeUri(context, directoryUri);
|
||||||
|
if (parent == null) return null;
|
||||||
|
filename = URLDecoder.decode(filename, DECODE_METHOD);
|
||||||
|
int extensionPosition = filename.lastIndexOf('.');
|
||||||
|
String extension = "";
|
||||||
|
if (extensionPosition > 0) {
|
||||||
|
extension = filename.substring(extensionPosition);
|
||||||
|
}
|
||||||
|
String mimeType = APPLICATION_OCTET_STREAM;
|
||||||
|
if (extension.equals(".txt")) {
|
||||||
|
mimeType = TEXT_PLAIN;
|
||||||
|
}
|
||||||
|
DocumentFile isExist = parent.findFile(filename);
|
||||||
|
if (isExist != null) return isExist;
|
||||||
|
return parent.createFile(mimeType, filename);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a directory from directory with filename.
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param directory parent path for directory.
|
||||||
|
* @param directoryName directory display name.
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static DocumentFile createDir(Context context, String directory, String directoryName) {
|
||||||
|
try {
|
||||||
|
Uri directoryUri = Uri.parse(directory);
|
||||||
|
DocumentFile parent;
|
||||||
|
parent = DocumentFile.fromTreeUri(context, directoryUri);
|
||||||
|
if (parent == null) return null;
|
||||||
|
directoryName = URLDecoder.decode(directoryName, DECODE_METHOD);
|
||||||
|
DocumentFile isExist = parent.findFile(directoryName);
|
||||||
|
if (isExist != null) return isExist;
|
||||||
|
return parent.createDirectory(directoryName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open content uri and return file descriptor to JNI.
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param path Native content uri path
|
||||||
|
* @param openmode will be one of "r", "r", "rw", "wa", "rwa"
|
||||||
|
* @return file descriptor
|
||||||
|
*/
|
||||||
|
public static int openContentUri(Context context, String path, String openmode) {
|
||||||
|
try (ParcelFileDescriptor parcelFileDescriptor =
|
||||||
|
context.getContentResolver().openFileDescriptor(Uri.parse(path), openmode)) {
|
||||||
|
if (parcelFileDescriptor == null) {
|
||||||
|
Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return parcelFileDescriptor.detachFd();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
|
||||||
|
* This function will be faster than DocumentFile.listFiles
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param uri Directory uri.
|
||||||
|
* @return CheapDocument lists.
|
||||||
|
*/
|
||||||
|
public static CheapDocument[] listFiles(Context context, Uri uri) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[]{
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||||
|
};
|
||||||
|
Cursor c = null;
|
||||||
|
final List<CheapDocument> results = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
String docId;
|
||||||
|
if (isRootTreeUri(uri)) {
|
||||||
|
docId = DocumentsContract.getTreeDocumentId(uri);
|
||||||
|
} else {
|
||||||
|
docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
}
|
||||||
|
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId);
|
||||||
|
c = resolver.query(childrenUri, columns, null, null, null);
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
final String documentId = c.getString(0);
|
||||||
|
final String documentName = c.getString(1);
|
||||||
|
final String documentMimeType = c.getString(2);
|
||||||
|
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
|
||||||
|
CheapDocument document = new CheapDocument(documentName, documentMimeType, documentUri);
|
||||||
|
results.add(document);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot list file error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return results.toArray(new CheapDocument[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether given path exists.
|
||||||
|
*
|
||||||
|
* @param path Native content uri path
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static boolean Exists(Context context, String path) {
|
||||||
|
Cursor c = null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_DOCUMENT_ID};
|
||||||
|
c = context.getContentResolver().query(mUri, columns, null, null, null);
|
||||||
|
return c.getCount() > 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether given path is a directory
|
||||||
|
*
|
||||||
|
* @param path content uri path
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static boolean isDirectory(Context context, String path) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_MIME_TYPE};
|
||||||
|
boolean isDirectory = false;
|
||||||
|
Cursor c = null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
c = resolver.query(mUri, columns, null, null, null);
|
||||||
|
c.moveToNext();
|
||||||
|
final String mimeType = c.getString(0);
|
||||||
|
isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return isDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file display name from given path
|
||||||
|
*
|
||||||
|
* @param path content uri path
|
||||||
|
* @return String display name
|
||||||
|
*/
|
||||||
|
public static String getFilename(Context context, String path) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_DISPLAY_NAME};
|
||||||
|
String filename = "";
|
||||||
|
Cursor c = null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
c = resolver.query(mUri, columns, null, null, null);
|
||||||
|
c.moveToNext();
|
||||||
|
filename = c.getString(0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] getFilesName(Context context, String path) {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
List<String> files = new ArrayList<>();
|
||||||
|
for (CheapDocument file : FileUtil.listFiles(context, uri)) {
|
||||||
|
files.add(file.getFilename());
|
||||||
|
}
|
||||||
|
return files.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file size from given path.
|
||||||
|
*
|
||||||
|
* @param path content uri path
|
||||||
|
* @return long file size
|
||||||
|
*/
|
||||||
|
public static long getFileSize(Context context, String path) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[] {DocumentsContract.Document.COLUMN_SIZE};
|
||||||
|
long size = 0;
|
||||||
|
Cursor c = null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
c = resolver.query(mUri, columns, null, null, null);
|
||||||
|
c.moveToNext();
|
||||||
|
size = c.getLong(0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean copyFile(Context context, String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||||
|
try {
|
||||||
|
Uri sourceUri = Uri.parse(sourcePath);
|
||||||
|
Uri destinationUri = Uri.parse(destinationParentPath);
|
||||||
|
DocumentFile destinationParent = DocumentFile.fromTreeUri(context, destinationUri);
|
||||||
|
if (destinationParent == null) return false;
|
||||||
|
String filename = URLDecoder.decode(destinationFilename, "UTF-8");
|
||||||
|
DocumentFile destination = destinationParent.findFile(filename);
|
||||||
|
if (destination == null) {
|
||||||
|
destination = destinationParent.createFile("application/octet-stream", filename);
|
||||||
|
}
|
||||||
|
if (destination == null) return false;
|
||||||
|
InputStream input = context.getContentResolver().openInputStream(sourceUri);
|
||||||
|
OutputStream output = context.getContentResolver().openOutputStream(destination.getUri());
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = input.read(buffer)) != -1) {
|
||||||
|
output.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
input.close();
|
||||||
|
output.flush();
|
||||||
|
output.close();
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot copy file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyDir(Context context, String sourcePath, String destinationPath,
|
||||||
|
CopyDirListener listener) {
|
||||||
|
try {
|
||||||
|
Uri sourceUri = Uri.parse(sourcePath);
|
||||||
|
Uri destinationUri = Uri.parse(destinationPath);
|
||||||
|
final List<Pair<CheapDocument, DocumentFile>> files = new ArrayList<>();
|
||||||
|
final List<Pair<Uri, Uri>> dirs = new ArrayList<>();
|
||||||
|
dirs.add(new Pair<>(sourceUri, destinationUri));
|
||||||
|
// Searching all files which need to be copied and struct the directory in destination.
|
||||||
|
while (!dirs.isEmpty()) {
|
||||||
|
DocumentFile fromDir = DocumentFile.fromTreeUri(context, dirs.get(0).first);
|
||||||
|
DocumentFile toDir = DocumentFile.fromTreeUri(context, dirs.get(0).second);
|
||||||
|
if (fromDir == null || toDir == null)
|
||||||
|
continue;
|
||||||
|
Uri fromUri = fromDir.getUri();
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onSearchProgress(fromUri.getPath());
|
||||||
|
}
|
||||||
|
CheapDocument[] documents = FileUtil.listFiles(context, fromUri);
|
||||||
|
for (CheapDocument document : documents) {
|
||||||
|
String filename = document.getFilename();
|
||||||
|
if (document.isDirectory()) {
|
||||||
|
DocumentFile target = toDir.findFile(filename);
|
||||||
|
if (target == null || !target.exists()) {
|
||||||
|
target = toDir.createDirectory(filename);
|
||||||
|
}
|
||||||
|
if (target == null)
|
||||||
|
continue;
|
||||||
|
dirs.add(new Pair<>(document.getUri(), target.getUri()));
|
||||||
|
} else {
|
||||||
|
DocumentFile target = toDir.findFile(filename);
|
||||||
|
if (target == null || !target.exists()) {
|
||||||
|
target =
|
||||||
|
toDir.createFile(document.getMimeType(), document.getFilename());
|
||||||
|
}
|
||||||
|
if (target == null)
|
||||||
|
continue;
|
||||||
|
files.add(new Pair<>(document, target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = files.size();
|
||||||
|
int progress = 0;
|
||||||
|
for (Pair<CheapDocument, DocumentFile> file : files) {
|
||||||
|
DocumentFile to = file.second;
|
||||||
|
Uri toUri = to.getUri();
|
||||||
|
String filename = getFilenameWithExtensions(toUri);
|
||||||
|
String toPath = toUri.getPath();
|
||||||
|
DocumentFile toParent = to.getParentFile();
|
||||||
|
if (toParent == null)
|
||||||
|
continue;
|
||||||
|
FileUtil.copyFile(context, file.first.getUri().toString(),
|
||||||
|
toParent.getUri().toString(), filename);
|
||||||
|
progress++;
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onCopyProgress(toPath, progress, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onComplete();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot copy directory, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean renameFile(Context context, String path, String destinationFilename) {
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
DocumentsContract.renameDocument(context.getContentResolver(), uri, destinationFilename);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot rename file, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean deleteDocument(Context context, String path) {
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
DocumentsContract.deleteDocument(context.getContentResolver(), uri);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot delete document, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] getBytesFromFile(File file) throws IOException {
|
public static byte[] getBytesFromFile(File file) throws IOException {
|
||||||
final long length = file.length();
|
final long length = file.length();
|
||||||
|
|
||||||
@@ -21,8 +395,8 @@ public class FileUtil {
|
|||||||
int numRead;
|
int numRead;
|
||||||
|
|
||||||
try (InputStream is = new FileInputStream(file)) {
|
try (InputStream is = new FileInputStream(file)) {
|
||||||
while (offset < bytes.length
|
while (offset < bytes.length &&
|
||||||
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
|
(numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
|
||||||
offset += numRead;
|
offset += numRead;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,4 +408,53 @@ public class FileUtil {
|
|||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isRootTreeUri(Uri uri) {
|
||||||
|
final List<String> paths = uri.getPathSegments();
|
||||||
|
return paths.size() == 2 && PATH_TREE.equals(paths.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isNativePath(String path) {
|
||||||
|
try {
|
||||||
|
return path.charAt(0) == '/';
|
||||||
|
} catch (StringIndexOutOfBoundsException e) {
|
||||||
|
Log.error("[FileUtil] Cannot determine the string is native path or not.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getFilenameWithExtensions(Uri uri) {
|
||||||
|
final String path = uri.getPath();
|
||||||
|
final int index = path.lastIndexOf('/');
|
||||||
|
return path.substring(index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double getFreeSpace(Context context, Uri uri) {
|
||||||
|
try {
|
||||||
|
Uri docTreeUri = DocumentsContract.buildDocumentUriUsingTree(
|
||||||
|
uri, DocumentsContract.getTreeDocumentId(uri));
|
||||||
|
ParcelFileDescriptor pfd =
|
||||||
|
context.getContentResolver().openFileDescriptor(docTreeUri, "r");
|
||||||
|
assert pfd != null;
|
||||||
|
StructStatVfs stats = Os.fstatvfs(pfd.getFileDescriptor());
|
||||||
|
double spaceInGigaBytes = stats.f_bavail * stats.f_bsize / 1024.0 / 1024 / 1024;
|
||||||
|
pfd.close();
|
||||||
|
return spaceInGigaBytes;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil] Cannot get storage size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeQuietly(AutoCloseable closeable) {
|
||||||
|
if (closeable != null) {
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (RuntimeException rethrown) {
|
||||||
|
throw rethrown;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ public class ForegroundService extends Service {
|
|||||||
private void showRunningNotification() {
|
private void showRunningNotification() {
|
||||||
// Intent is used to resume emulation if the notification is clicked
|
// Intent is used to resume emulation if the notification is clicked
|
||||||
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
|
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
|
||||||
new Intent(this, EmulationActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
|
new Intent(this, EmulationActivity.class), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
|
||||||
.setSmallIcon(R.drawable.ic_stat_notification_logo)
|
.setSmallIcon(R.drawable.ic_stat_notification_logo)
|
||||||
|
@@ -13,12 +13,12 @@ import java.nio.IntBuffer;
|
|||||||
public class GameIconRequestHandler extends RequestHandler {
|
public class GameIconRequestHandler extends RequestHandler {
|
||||||
@Override
|
@Override
|
||||||
public boolean canHandleRequest(Request data) {
|
public boolean canHandleRequest(Request data) {
|
||||||
return "iso".equals(data.uri.getScheme());
|
return "content".equals(data.uri.getScheme()) || data.uri.getScheme() == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result load(Request request, int networkPolicy) {
|
public Result load(Request request, int networkPolicy) {
|
||||||
String url = request.uri.getHost() + request.uri.getPath();
|
String url = request.uri.toString();
|
||||||
int[] vector = NativeLibrary.GetIcon(url);
|
int[] vector = NativeLibrary.GetIcon(url);
|
||||||
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
|
Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565);
|
||||||
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));
|
bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector));
|
||||||
|
@@ -0,0 +1,33 @@
|
|||||||
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
|
||||||
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
|
|
||||||
|
public class InsetsHelper {
|
||||||
|
public static final int THREE_BUTTON_NAVIGATION = 0;
|
||||||
|
public static final int TWO_BUTTON_NAVIGATION = 1;
|
||||||
|
public static final int GESTURE_NAVIGATION = 2;
|
||||||
|
|
||||||
|
public static void insetAppBar(Insets insets, AppBarLayout appBarLayout)
|
||||||
|
{
|
||||||
|
ViewGroup.MarginLayoutParams mlpAppBar =
|
||||||
|
(ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
|
||||||
|
mlpAppBar.leftMargin = insets.left;
|
||||||
|
mlpAppBar.rightMargin = insets.right;
|
||||||
|
appBarLayout.setLayoutParams(mlpAppBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSystemGestureType(Context context) {
|
||||||
|
Resources resources = context.getResources();
|
||||||
|
int resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android");
|
||||||
|
if (resourceId != 0) {
|
||||||
|
return resources.getInteger(resourceId);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,28 +1,32 @@
|
|||||||
package org.citra.citra_emu.utils;
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
import org.citra.citra_emu.CitraApplication;
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
|
||||||
public class PermissionsHandler {
|
public class PermissionsHandler {
|
||||||
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
|
public static final String CITRA_DIRECTORY = "CITRA_DIRECTORY";
|
||||||
|
public static final SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||||
|
|
||||||
// We use permissions acceptance as an indicator if this is a first boot for the user.
|
// We use permissions acceptance as an indicator if this is a first boot for the user.
|
||||||
public static boolean isFirstBoot(final FragmentActivity activity) {
|
public static boolean isFirstBoot(FragmentActivity activity) {
|
||||||
return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
|
return !hasWriteAccess(activity.getApplicationContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
public static boolean checkWritePermission(FragmentActivity activity,
|
||||||
public static boolean checkWritePermission(final FragmentActivity activity) {
|
ActivityResultLauncher<Uri> launcher) {
|
||||||
if (isFirstBoot(activity)) {
|
if (isFirstBoot(activity)) {
|
||||||
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
|
launcher.launch(null);
|
||||||
REQUEST_CODE_WRITE_PERMISSION);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +34,31 @@ public class PermissionsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasWriteAccess(Context context) {
|
public static boolean hasWriteAccess(Context context) {
|
||||||
return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
try {
|
||||||
|
Uri uri = getCitraDirectory();
|
||||||
|
if (uri == null)
|
||||||
|
return false;
|
||||||
|
int takeFlags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
|
||||||
|
DocumentFile root = DocumentFile.fromTreeUri(context, uri);
|
||||||
|
if (root != null && root.exists()) return true;
|
||||||
|
context.getContentResolver().releasePersistableUriPermission(uri, takeFlags);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[PermissionsHandler]: Cannot check citra data directory permission, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Uri getCitraDirectory() {
|
||||||
|
String directoryString = mPreferences.getString(CITRA_DIRECTORY, "");
|
||||||
|
if (directoryString.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Uri.parse(directoryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean setCitraDirectory(String uriString) {
|
||||||
|
return mPreferences.edit().putString(CITRA_DIRECTORY, uriString).commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ public class PicassoUtils {
|
|||||||
public static void loadGameIcon(ImageView imageView, String gamePath) {
|
public static void loadGameIcon(ImageView imageView, String gamePath) {
|
||||||
Picasso
|
Picasso
|
||||||
.get()
|
.get()
|
||||||
.load(Uri.parse("iso:/" + gamePath))
|
.load(Uri.parse(gamePath))
|
||||||
.fit()
|
.fit()
|
||||||
.centerInside()
|
.centerInside()
|
||||||
.config(Bitmap.Config.RGB_565)
|
.config(Bitmap.Config.RGB_565)
|
||||||
|
@@ -1,21 +1,23 @@
|
|||||||
package org.citra.citra_emu.utils;
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
import org.citra.citra_emu.activities.EmulationActivity;
|
||||||
|
|
||||||
public final class StartupHandler {
|
public final class StartupHandler {
|
||||||
private static void handlePermissionsCheck(FragmentActivity parent) {
|
private static void handlePermissionsCheck(FragmentActivity parent,
|
||||||
|
ActivityResultLauncher<Uri> launcher) {
|
||||||
// Ask the user to grant write permission if it's not already granted
|
// Ask the user to grant write permission if it's not already granted
|
||||||
PermissionsHandler.checkWritePermission(parent);
|
PermissionsHandler.checkWritePermission(parent, launcher);
|
||||||
|
|
||||||
String start_file = "";
|
String start_file = "";
|
||||||
Bundle extras = parent.getIntent().getExtras();
|
Bundle extras = parent.getIntent().getExtras();
|
||||||
@@ -32,16 +34,23 @@ public final class StartupHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void HandleInit(FragmentActivity parent) {
|
public static void HandleInit(FragmentActivity parent, ActivityResultLauncher<Uri> launcher) {
|
||||||
if (PermissionsHandler.isFirstBoot(parent)) {
|
if (PermissionsHandler.isFirstBoot(parent)) {
|
||||||
// Prompt user with standard first boot disclaimer
|
// Prompt user with standard first boot disclaimer
|
||||||
new MaterialAlertDialogBuilder(parent)
|
AlertDialog dialog =
|
||||||
|
new MaterialAlertDialogBuilder(parent)
|
||||||
.setTitle(R.string.app_name)
|
.setTitle(R.string.app_name)
|
||||||
.setIcon(R.mipmap.ic_launcher)
|
.setIcon(R.mipmap.ic_launcher)
|
||||||
.setMessage(parent.getResources().getString(R.string.app_disclaimer))
|
.setMessage(R.string.app_disclaimer)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent))
|
.setCancelable(false)
|
||||||
|
.setOnDismissListener(
|
||||||
|
dialogInterface -> handlePermissionsCheck(parent, launcher))
|
||||||
.show();
|
.show();
|
||||||
|
TextView textView = dialog.findViewById(android.R.id.message);
|
||||||
|
if (textView == null)
|
||||||
|
return;
|
||||||
|
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,31 @@
|
|||||||
package org.citra.citra_emu.utils;
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.Color;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.view.WindowCompat;
|
import androidx.core.view.WindowCompat;
|
||||||
import androidx.core.view.WindowInsetsControllerCompat;
|
import androidx.core.view.WindowInsetsControllerCompat;
|
||||||
|
|
||||||
|
import com.google.android.material.color.MaterialColors;
|
||||||
|
|
||||||
import org.citra.citra_emu.CitraApplication;
|
import org.citra.citra_emu.CitraApplication;
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||||
|
|
||||||
public class ThemeUtil {
|
public class ThemeUtil {
|
||||||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||||
|
|
||||||
|
public static final float NAV_BAR_ALPHA = 0.9f;
|
||||||
|
|
||||||
private static void applyTheme(int designValue, AppCompatActivity activity) {
|
private static void applyTheme(int designValue, AppCompatActivity activity) {
|
||||||
switch (designValue) {
|
switch (designValue) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -34,9 +44,40 @@ public class ThemeUtil {
|
|||||||
int systemReportedThemeMode = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
int systemReportedThemeMode = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
WindowInsetsControllerCompat windowController = WindowCompat.getInsetsController(activity.getWindow(), activity.getWindow().getDecorView());
|
WindowInsetsControllerCompat windowController = WindowCompat.getInsetsController(activity.getWindow(), activity.getWindow().getDecorView());
|
||||||
windowController.setAppearanceLightStatusBars(systemReportedThemeMode == Configuration.UI_MODE_NIGHT_NO);
|
windowController.setAppearanceLightStatusBars(systemReportedThemeMode == Configuration.UI_MODE_NIGHT_NO);
|
||||||
|
windowController.setAppearanceLightNavigationBars(systemReportedThemeMode == Configuration.UI_MODE_NIGHT_NO);
|
||||||
|
|
||||||
|
setNavigationBarColor(activity, MaterialColors.getColor(activity.getWindow().getDecorView(), R.attr.colorSurface));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void applyTheme(AppCompatActivity activity) {
|
public static void applyTheme(AppCompatActivity activity) {
|
||||||
applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0), activity);
|
applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0), activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setNavigationBarColor(@NonNull Activity activity, @ColorInt int color) {
|
||||||
|
int gestureType = InsetsHelper.getSystemGestureType(activity.getApplicationContext());
|
||||||
|
int orientation = activity.getResources().getConfiguration().orientation;
|
||||||
|
|
||||||
|
// Use a solid color when the navigation bar is on the left/right edge of the screen
|
||||||
|
if ((gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
|
||||||
|
gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION) &&
|
||||||
|
orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
activity.getWindow().setNavigationBarColor(color);
|
||||||
|
} else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION ||
|
||||||
|
gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION) {
|
||||||
|
// Use semi-transparent color when in portrait mode with three/two button navigation to
|
||||||
|
// partially see list items behind the navigation bar
|
||||||
|
activity.getWindow().setNavigationBarColor(ThemeUtil.getColorWithOpacity(color, NAV_BAR_ALPHA));
|
||||||
|
} else {
|
||||||
|
// Use transparent color when using gesture navigation
|
||||||
|
activity.getWindow().setNavigationBarColor(
|
||||||
|
ContextCompat.getColor(activity.getApplicationContext(),
|
||||||
|
android.R.color.transparent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
public static int getColorWithOpacity(@ColorInt int color, float alphaFactor) {
|
||||||
|
return Color.argb(Math.round(alphaFactor * Color.alpha(color)), Color.red(color),
|
||||||
|
Color.green(color), Color.blue(color));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,11 @@
|
|||||||
Config::Config() {
|
Config::Config() {
|
||||||
// TODO: Don't hardcode the path; let the frontend decide where to put the config files.
|
// TODO: Don't hardcode the path; let the frontend decide where to put the config files.
|
||||||
sdl2_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "config.ini";
|
sdl2_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "config.ini";
|
||||||
sdl2_config = std::make_unique<INIReader>(sdl2_config_loc);
|
std::string ini_buffer;
|
||||||
|
FileUtil::ReadFileToString(true, sdl2_config_loc, ini_buffer);
|
||||||
|
if (!ini_buffer.empty()) {
|
||||||
|
sdl2_config = std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size());
|
||||||
|
}
|
||||||
|
|
||||||
Reload();
|
Reload();
|
||||||
}
|
}
|
||||||
@@ -35,12 +39,15 @@ Config::~Config() = default;
|
|||||||
|
|
||||||
bool Config::LoadINI(const std::string& default_contents, bool retry) {
|
bool Config::LoadINI(const std::string& default_contents, bool retry) {
|
||||||
const std::string& location = this->sdl2_config_loc;
|
const std::string& location = this->sdl2_config_loc;
|
||||||
if (sdl2_config->ParseError() < 0) {
|
if (sdl2_config == nullptr || sdl2_config->ParseError() < 0) {
|
||||||
if (retry) {
|
if (retry) {
|
||||||
LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location);
|
LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location);
|
||||||
FileUtil::CreateFullPath(location);
|
FileUtil::CreateFullPath(location);
|
||||||
FileUtil::WriteStringToFile(true, location, default_contents);
|
FileUtil::WriteStringToFile(true, location, default_contents);
|
||||||
sdl2_config = std::make_unique<INIReader>(location); // Reopen file
|
std::string ini_buffer;
|
||||||
|
FileUtil::ReadFileToString(true, location, ini_buffer);
|
||||||
|
sdl2_config =
|
||||||
|
std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size()); // Reopen file
|
||||||
|
|
||||||
return LoadINI(default_contents, false);
|
return LoadINI(default_contents, false);
|
||||||
}
|
}
|
||||||
|
@@ -180,9 +180,12 @@ factor_3d =
|
|||||||
|
|
||||||
# The name of the post processing shader to apply.
|
# The name of the post processing shader to apply.
|
||||||
# Loaded from shaders if render_3d is off or side by side.
|
# Loaded from shaders if render_3d is off or side by side.
|
||||||
# Loaded from shaders/anaglyph if render_3d is anaglyph
|
|
||||||
pp_shader_name =
|
pp_shader_name =
|
||||||
|
|
||||||
|
# The name of the shader to apply when render_3d is anaglyph.
|
||||||
|
# Loaded from shaders/anaglyph
|
||||||
|
anaglyph_shader_name =
|
||||||
|
|
||||||
# Whether to enable linear filtering or not
|
# Whether to enable linear filtering or not
|
||||||
# This is required for some shaders to work correctly
|
# This is required for some shaders to work correctly
|
||||||
# 0: Nearest, 1 (default): Linear
|
# 0: Nearest, 1 (default): Linear
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/android_storage.h"
|
||||||
#include "common/common_paths.h"
|
#include "common/common_paths.h"
|
||||||
#include "common/logging/backend.h"
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/filter.h"
|
#include "common/logging/filter.h"
|
||||||
@@ -159,10 +160,6 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|||||||
log_filter.ParseFilterString(Settings::values.log_filter.GetValue());
|
log_filter.ParseFilterString(Settings::values.log_filter.GetValue());
|
||||||
Log::SetGlobalFilter(log_filter);
|
Log::SetGlobalFilter(log_filter);
|
||||||
Log::AddBackend(std::make_unique<Log::LogcatBackend>());
|
Log::AddBackend(std::make_unique<Log::LogcatBackend>());
|
||||||
FileUtil::CreateFullPath(FileUtil::GetUserPath(FileUtil::UserPath::LogDir));
|
|
||||||
Log::AddBackend(std::make_unique<Log::FileBackend>(
|
|
||||||
FileUtil::GetUserPath(FileUtil::UserPath::LogDir) + LOG_FILE));
|
|
||||||
LOG_INFO(Frontend, "Logging backend initialised");
|
|
||||||
|
|
||||||
// Initialize misc classes
|
// Initialize misc classes
|
||||||
s_savestate_info_class = reinterpret_cast<jclass>(
|
s_savestate_info_class = reinterpret_cast<jclass>(
|
||||||
@@ -230,6 +227,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|||||||
MiiSelector::InitJNI(env);
|
MiiSelector::InitJNI(env);
|
||||||
SoftwareKeyboard::InitJNI(env);
|
SoftwareKeyboard::InitJNI(env);
|
||||||
Camera::StillImage::InitJNI(env);
|
Camera::StillImage::InitJNI(env);
|
||||||
|
AndroidStorage::InitJNI(env, s_native_library_class);
|
||||||
|
|
||||||
return JNI_VERSION;
|
return JNI_VERSION;
|
||||||
}
|
}
|
||||||
@@ -254,6 +252,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
|||||||
MiiSelector::CleanupJNI(env);
|
MiiSelector::CleanupJNI(env);
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
Camera::StillImage::CleanupJNI(env);
|
Camera::StillImage::CleanupJNI(env);
|
||||||
|
AndroidStorage::CleanupJNI();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
#include "audio_core/dsp_interface.h"
|
#include "audio_core/dsp_interface.h"
|
||||||
#include "common/aarch64/cpu_detect.h"
|
#include "common/aarch64/cpu_detect.h"
|
||||||
|
#include "common/common_paths.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/microprofile.h"
|
#include "common/microprofile.h"
|
||||||
#include "common/scm_rev.h"
|
#include "common/scm_rev.h"
|
||||||
@@ -329,6 +331,8 @@ jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetInstalledGamePaths(
|
|||||||
path += '/';
|
path += '/';
|
||||||
FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir);
|
FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir);
|
||||||
} else {
|
} else {
|
||||||
|
if (!FileUtil::Exists(path))
|
||||||
|
return false;
|
||||||
auto loader = Loader::GetLoader(path);
|
auto loader = Loader::GetLoader(path);
|
||||||
if (loader) {
|
if (loader) {
|
||||||
bool executable{};
|
bool executable{};
|
||||||
@@ -502,6 +506,23 @@ void Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
|
|||||||
Config{};
|
Config{};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Java_org_citra_citra_1emu_NativeLibrary_CreateLogFile(JNIEnv* env,
|
||||||
|
[[maybe_unused]] jclass clazz) {
|
||||||
|
Log::RemoveBackend(Log::FileBackend::Name());
|
||||||
|
FileUtil::CreateFullPath(FileUtil::GetUserPath(FileUtil::UserPath::LogDir));
|
||||||
|
Log::AddBackend(std::make_unique<Log::FileBackend>(
|
||||||
|
FileUtil::GetUserPath(FileUtil::UserPath::LogDir) + LOG_FILE));
|
||||||
|
LOG_INFO(Frontend, "Logging backend initialised");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_citra_citra_1emu_NativeLibrary_LogUserDirectory(JNIEnv* env,
|
||||||
|
[[maybe_unused]] jclass clazz,
|
||||||
|
jstring j_path) {
|
||||||
|
std::string_view path = env->GetStringUTFChars(j_path, 0);
|
||||||
|
LOG_INFO(Frontend, "User directory path: {}", path);
|
||||||
|
env->ReleaseStringUTFChars(j_path, path.data());
|
||||||
|
}
|
||||||
|
|
||||||
jint Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
|
jint Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
|
||||||
[[maybe_unused]] jclass clazz) {
|
[[maybe_unused]] jclass clazz) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@@ -83,6 +83,13 @@ JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetSysDirectory(J
|
|||||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
|
||||||
jclass clazz);
|
jclass clazz);
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateLogFile(JNIEnv* env,
|
||||||
|
jclass clazz);
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LogUserDirectory(JNIEnv* env,
|
||||||
|
jclass clazz,
|
||||||
|
jstring path);
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
|
JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
|
||||||
jclass clazz);
|
jclass clazz);
|
||||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetProfiling(JNIEnv* env,
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetProfiling(JNIEnv* env,
|
||||||
|
255
src/android/app/src/main/res/drawable/ic_citra.xml
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="500"
|
||||||
|
android:viewportHeight="500">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M219.74,384.99C207.69,383.48 190.3,379.54 180.01,375.99C149.58,365.5 118.62,341.56 104.78,317.82C99.82,309.33 98.17,298.35 99.66,283.76C100.71,273.41 102.5,267.04 106.99,257.64C120.93,228.46 144.24,200.66 174.41,177.25C197.13,159.63 221.04,145.17 242.51,136.07C260.96,128.25 284,121.64 298.84,119.91C317.16,117.76 336.42,120.69 348.85,127.49C361.42,134.38 369.36,144.5 378.28,164.97C386.65,184.19 389.88,202.02 389.86,228.93C389.85,251.76 387.07,268.16 380.29,285.33C374.66,299.59 365.74,314.82 355.81,327.13C351.38,332.61 337.9,345.93 332.06,350.58C318.77,361.16 301.47,370.94 287.67,375.67C276.18,379.6 260.88,383.13 248.04,384.79C241.56,385.63 225.73,385.74 219.74,384.99z" />
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M237.24,238.74C235.5,247.54 235.3,256.45 235.32,266.13C235.42,270.41 235.32,275.43 235.86,280.46C236.98,290.68 240.45,293.03 249.78,288.93C265.7,281.95 279.81,272.16 292.42,260.21C295.98,256.84 299.72,253.59 302.3,249.33C304.69,245.41 304.17,243.2 300.29,240.77C299.04,239.99 297.66,239.4 296.27,238.9C291.66,237.22 286.88,236.12 282.09,235.08C270.84,232.66 259.61,230.05 248.04,229.89C247.96,229.89 247.89,229.89 247.82,229.89C242.07,229.89 238.35,233.1 237.24,238.74">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="237.57"
|
||||||
|
android:centerY="226.48"
|
||||||
|
android:gradientRadius="112.82"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="0.67" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M222.5,240.44C219.61,241.88 217.31,244.14 215.05,246.4C199.92,261.46 185.65,277.32 171.68,293.46C167.88,297.85 163.97,302.22 161.54,307.62C159.83,311.42 160.71,313.37 164.8,314.45C169.01,315.56 173.3,316.04 177.66,315.66C188.09,314.75 197.98,311.81 207.65,307.93C216.6,304.34 222.21,297.97 224.18,288.47C226.75,276.01 227.66,263.36 228.29,250.69C228.29,248.75 228.31,246.81 228.28,244.86C228.27,244.29 228.15,243.73 228.04,243.17C227.55,240.74 226.75,239.59 225.27,239.59C224.53,239.59 223.62,239.88 222.5,240.44">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="224.1"
|
||||||
|
android:centerY="236.89"
|
||||||
|
android:gradientRadius="82.58"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M339.84,178.38C337.27,178.84 334.73,179.56 332.26,180.41C318.25,185.3 304.95,191.86 291.47,197.97C279.95,203.19 268.45,208.47 257.53,214.91C255.38,216.18 252.76,217.68 253.28,220.55C253.73,223 256.63,223.03 258.64,223.5C274.1,227.13 289.84,228.59 304.37,229.79C313.71,229.69 321.24,228.2 327.11,221.93C333.24,215.37 338.37,208.12 342.19,200C344.58,194.91 346.18,189.5 346.18,183.81C346.18,180.11 344.54,178.22 341.48,178.22C340.97,178.22 340.43,178.28 339.84,178.38">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="270"
|
||||||
|
android:centerY="215"
|
||||||
|
android:gradientRadius="66.51"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M208.07,237.99C207.75,238.09 207.44,238.25 207.13,238.38C187.03,246.55 167.73,256.43 148.43,266.29C143.56,268.78 138.86,271.58 134.46,274.89C129.74,278.44 127.05,282.9 127.64,288.88C127.64,289.67 127.61,290.36 127.64,291.04C128,297.84 131.43,302.61 137.28,305.8C142.71,308.76 147.39,306.84 151.86,303.55C152.96,302.74 154.09,301.95 155.11,301.05C175.47,283.13 193.68,263.12 211.56,242.79C212.69,241.51 214.63,239.99 213.35,238.07C212.81,237.24 212.17,236.97 211.48,236.97C210.38,236.97 209.17,237.68 208.07,237.99">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="212.16"
|
||||||
|
android:centerY="237.77"
|
||||||
|
android:gradientRadius="73.42"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M309.87,147.88C307.95,149.3 306.15,150.89 304.35,152.47C291.58,163.74 280.22,176.44 268.48,188.74C263.6,193.85 258.79,199.07 254.56,204.76C253.83,205.76 252.91,206.87 253.82,208.17C254.69,209.4 256.07,209.19 257.27,208.83C258.9,208.33 260.5,207.69 262.07,207.03C283.2,198.1 303.64,187.73 324,177.21C329.85,174.18 335.75,171.1 340.56,166.4C342.36,164.64 343.48,162.62 343.54,159.77C343.48,157.72 342.7,155.59 341.56,153.57C337.94,147.16 330.59,143.63 323.11,143.63C318.46,143.63 313.77,144.99 309.87,147.88">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="228.5"
|
||||||
|
android:centerY="223.73"
|
||||||
|
android:gradientRadius="181.47"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFEE05"
|
||||||
|
android:offset="0.5" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M152.4,226.49C147.18,232.43 142.24,238.54 138.22,245.36C135.66,249.7 133.61,254.32 132.56,259.29C131.83,262.73 132.92,264.15 135.86,264.2C137.72,264.18 139.46,263.66 141.18,263.04C158.06,257 174.12,249.06 190.35,241.5C197.27,238.28 204.15,234.93 210.65,230.89C212.6,229.67 214.78,228.22 214.38,225.62C214.03,223.3 211.39,223.55 209.7,222.79C209.08,222.52 208.39,222.4 207.72,222.25C197.27,219.78 186.62,218.65 175.98,217.52C174.75,217.39 173.54,217.32 172.36,217.32C164.6,217.32 157.94,220.18 152.4,226.49">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="221.12"
|
||||||
|
android:centerY="227.29"
|
||||||
|
android:gradientRadius="82.14"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M289.75,141.89C277.12,143.2 265.34,147.29 253.93,152.69C247.04,155.94 242.73,161.14 241.32,168.68C239.72,177.24 238.61,185.84 238.73,194.57C238.73,196.97 238.67,199.37 238.75,201.77C238.81,203.55 238.4,205.69 240.38,206.68C242.36,207.67 244.26,206.6 245.78,205.44C248.31,203.5 250.74,201.4 253,199.15C265.53,186.65 277.31,173.45 289.02,160.19C292.63,156.1 296.43,152.08 298.79,147.02C300.21,143.97 299.51,142.59 296.22,141.94C295.23,141.75 294.23,141.68 293.23,141.68C292.07,141.68 290.91,141.78 289.75,141.89">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="254.27"
|
||||||
|
android:centerY="200.99"
|
||||||
|
android:gradientRadius="122.34"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFBA05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M223.76,167.89C220.24,168.99 217.12,170.86 213.95,172.68C200.94,180.13 188.86,188.83 178.5,199.77C173.7,204.85 174.46,208.17 181.12,210.07C193.5,213.61 206.07,216.34 219.01,216.66C225.82,216.83 229.25,213.65 230.47,207.01C231.71,200.22 231.97,193.4 231.79,184.38C231.6,181.64 232.49,176.67 231.18,171.86C230.34,168.76 228.93,167.38 226.6,167.38C225.78,167.38 224.83,167.56 223.76,167.89">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="242.5"
|
||||||
|
android:centerY="226.29"
|
||||||
|
android:gradientRadius="77.33"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF00"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFCF05"
|
||||||
|
android:offset="0.63" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFCF05"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M356.04,157.69C356.25,158.22 356.29,158.38 356.37,158.63C356.53,159.1 356.62,159.36 356.7,159.61C356.78,159.86 356.72,159.72 356.8,159.96C356.87,160.21 356.96,160.48 357.03,160.72C357.1,160.97 357.17,161.31 357.23,161.55C357.3,161.8 357.38,162.09 357.44,162.34C357.5,162.58 357.67,163.53 357.72,163.78C358,165.2 357.93,165.67 358.07,167.08C358.22,168.48 358.29,169.88 358.31,171.27C358.32,172.67 358.27,174.05 358.17,175.43C358.06,176.8 357.91,178.17 357.69,179.54C357.48,180.9 357.22,182.25 356.91,183.6C356.61,184.95 356.25,186.29 355.86,187.63C355.46,188.96 355.03,190.29 354.56,191.62C354.09,192.94 353.58,194.26 353.05,195.57C352,198.15 350.87,200.68 349.66,203.15C348.45,205.63 347.17,208.06 345.83,210.43C344.48,212.81 343.06,215.15 341.59,217.44C340.11,219.73 338.58,221.97 336.99,224.19C335.4,226.4 333.75,228.57 332.06,230.71C330.36,232.85 328.62,234.95 326.84,237.02C325.06,239.1 323.24,241.14 321.38,243.16C319.52,245.18 317.63,247.18 315.71,249.15C313.74,251.18 311.73,253.18 309.69,255.14C307.65,257.09 305.58,259.01 303.49,260.9C301.39,262.79 299.26,264.64 297.11,266.46C294.96,268.28 292.78,270.06 290.58,271.82C288.37,273.57 286.14,275.3 283.89,276.99C281.64,278.69 279.36,280.35 277.06,281.99C274.76,283.63 272.44,285.24 270.1,286.82C267.75,288.41 265.39,289.97 263.01,291.5C260.04,293.41 257.06,295.26 254.04,297.05C251.03,298.83 247.99,300.56 244.93,302.23C241.86,303.89 238.77,305.49 235.64,307.02C232.52,308.55 229.37,310.02 226.19,311.41C223.01,312.8 219.79,314.12 216.55,315.37C214.9,316 213.25,316.62 211.59,317.21C209.97,317.79 208.35,318.34 206.72,318.88C203.4,319.98 200.06,321 196.68,321.93C193.3,322.87 189.88,323.72 186.43,324.49C184.98,324.81 183.53,325.11 182.08,325.38C180.64,325.65 179.18,325.9 177.73,326.12C176.28,326.33 174.82,326.52 173.37,326.68C171.91,326.85 170.46,326.98 169,327.08C167.54,327.18 166.08,327.26 164.62,327.3C163.16,327.34 161.69,327.35 160.23,327.33C158.77,327.31 157.3,327.25 155.84,327.17C154.37,327.08 152.9,326.96 151.44,326.8C150.25,326.68 149.06,326.52 147.89,326.33C146.72,326.14 145.56,325.91 144.41,325.65C143.26,325.39 142.13,325.09 141.01,324.76C139.89,324.42 138.78,324.04 137.7,323.62C136.61,323.19 135.54,322.73 134.49,322.22C133.82,321.89 133.16,321.55 132.51,321.18C132.04,320.93 131.58,320.66 131.13,320.39C130.12,319.78 129.13,319.12 128.16,318.41C127.5,317.92 126.85,317.41 126.21,316.87C125.96,316.67 125.71,316.47 125.49,316.28C130.1,321.36 134.85,326.09 139.73,330.45C144.67,334.86 149.74,338.9 154.95,342.55C160.15,346.2 165.5,349.45 170.97,352.31C176.45,355.17 182.07,357.62 187.82,359.65C193.57,361.68 199.45,363.3 205.47,364.47C211.5,365.66 217.65,366.4 223.95,366.7C230.24,366.99 236.67,366.84 243.24,366.23C249.8,365.61 256.5,364.53 263.34,362.97C270.63,361.31 277.58,359.26 284.17,356.84C290.76,354.41 297,351.61 302.89,348.45C308.78,345.28 314.32,341.75 319.52,337.85C324.71,333.96 329.55,329.71 334.05,325.1C338.54,320.5 342.69,315.54 346.49,310.24C350.29,304.94 353.74,299.3 356.85,293.32C359.95,287.35 362.72,281.04 365.13,274.4C367.55,267.76 369.62,260.8 371.35,253.51C371.83,251.46 372.26,249.39 372.62,247.33C372.99,245.26 373.29,243.19 373.53,241.12C373.78,239.04 373.97,236.97 374.1,234.89C374.23,232.81 374.32,230.73 374.35,228.64C374.38,226.56 374.36,224.47 374.29,222.38C374.23,220.29 374.11,218.2 373.96,216.11C373.8,214.02 373.61,211.93 373.37,209.83C373.13,207.74 372.85,205.65 372.54,203.55C372.19,201.22 371.73,198.79 371.17,196.31C370.62,193.83 369.97,191.31 369.26,188.8C368.56,186.29 367.78,183.79 366.97,181.35C366.17,178.92 365.32,176.55 364.47,174.3C363.61,172.05 362.74,169.93 361.9,167.97C361.05,166.02 360.21,164.25 359.43,162.71C358.64,161.17 357.89,159.86 357.21,158.84C356.79,158.2 356.1,157.21 355.48,156.36C355.67,156.78 355.87,157.23 356.04,157.69">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="230"
|
||||||
|
android:centerY="300"
|
||||||
|
android:gradientRadius="100"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF87"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFFF87"
|
||||||
|
android:offset="0.13" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFC503"
|
||||||
|
android:offset="0.14" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFC503"
|
||||||
|
android:offset="0.22" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFC503"
|
||||||
|
android:offset="0.4" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFC503"
|
||||||
|
android:offset="0.44" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFC003"
|
||||||
|
android:offset="0.5" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFC003"
|
||||||
|
android:offset="0.63" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFFC003"
|
||||||
|
android:offset="0.73" />
|
||||||
|
<item
|
||||||
|
android:color="#DDFF8904"
|
||||||
|
android:offset="0.85" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFF8904"
|
||||||
|
android:offset="0.99" />
|
||||||
|
<item
|
||||||
|
android:color="#FFFF8904"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<group>
|
||||||
|
<clip-path android:pathData="M354.7,154.83C355.02,155.44 355.6,156.52 356.04,157.7C356.25,158.23 356.29,158.39 356.37,158.64C356.53,159.11 356.62,159.37 356.7,159.62C356.78,159.87 356.72,159.73 356.8,159.97C356.87,160.22 356.96,160.49 357.03,160.73C357.1,160.98 357.17,161.32 357.23,161.56C357.3,161.81 357.38,162.1 357.44,162.35C357.5,162.6 357.67,163.54 357.72,163.79C358,165.21 357.93,165.68 358.07,167.09C358.22,168.49 358.29,169.89 358.31,171.29C358.32,172.68 358.27,174.06 358.17,175.44C358.06,176.82 357.91,178.18 357.69,179.55C357.48,180.91 357.22,182.26 356.91,183.61C356.61,184.96 356.25,186.3 355.86,187.64C355.46,188.97 355.03,190.3 354.56,191.63C354.09,192.95 353.58,194.27 353.05,195.58C352,198.16 350.87,200.69 349.66,203.17C348.45,205.64 347.17,208.07 345.83,210.45C344.48,212.83 343.06,215.16 341.59,217.45C340.11,219.74 338.58,221.99 336.99,224.2C335.4,226.41 333.75,228.58 332.06,230.72C330.36,232.86 328.62,234.96 326.84,237.04C325.06,239.11 323.24,241.16 321.38,243.18C319.52,245.2 317.63,247.19 315.71,249.16C313.74,251.2 311.73,253.19 309.69,255.15C307.65,257.1 305.58,259.03 303.49,260.91C301.39,262.8 299.26,264.65 297.11,266.47C294.96,268.29 292.78,270.07 290.58,271.83C288.37,273.58 286.14,275.31 283.89,277C281.64,278.7 279.36,280.36 277.06,282C274.76,283.64 272.44,285.25 270.1,286.83C267.75,288.42 265.39,289.98 263.01,291.51C260.04,293.42 257.06,295.27 254.04,297.06C251.03,298.85 247.99,300.57 244.93,302.24C241.86,303.9 238.77,305.5 235.64,307.03C232.52,308.56 229.37,310.03 226.19,311.42C223.01,312.81 219.79,314.13 216.55,315.38C214.9,316.01 213.25,316.63 211.59,317.22C209.97,317.8 208.35,318.36 206.72,318.9C203.4,319.99 200.06,321.01 196.68,321.94C193.3,322.88 189.88,323.73 186.43,324.5C184.98,324.82 183.53,325.12 182.08,325.39C180.64,325.66 179.18,325.91 177.73,326.13C176.28,326.34 174.82,326.53 173.37,326.7C171.91,326.86 170.46,326.99 169,327.09C167.54,327.2 166.08,327.27 164.62,327.31C163.16,327.35 161.69,327.36 160.23,327.34C158.77,327.32 157.3,327.26 155.84,327.18C154.37,327.09 152.9,326.97 151.44,326.81C150.25,326.69 149.06,326.53 147.89,326.34C146.72,326.15 145.56,325.93 144.41,325.66C143.26,325.4 142.13,325.11 141.01,324.77C139.89,324.43 138.78,324.05 137.7,323.63C136.61,323.2 135.54,322.74 134.49,322.23C133.44,321.72 132.4,321.16 131.39,320.55C130.38,319.94 129.39,319.28 128.43,318.57C127.46,317.86 126.06,316.81 125.15,315.99L122.34,322.46C119.54,328.93 123.68,327.81 125.15,330.04C126.61,332.26 125.46,338.06 125.34,343.4C125.21,348.75 131.26,353.65 133.93,356.32C136.6,359 142.84,365.68 146.85,366.86C150.86,368.03 167.79,381.27 173.58,383.06C179.38,384.84 185.61,389.29 193.19,390.63C200.76,391.97 210.12,393.3 217.69,393.3C225.27,393.3 241.31,393.75 252,393.3C262.69,392.86 276.06,390.63 281.41,389.74C286.75,388.85 299.23,387.51 305.91,385.73C312.59,383.95 326.85,378.6 329.97,375.48C333.09,372.36 354.92,363.01 359.82,357.66C364.72,352.31 381.21,322.46 382.1,317.12C382.99,311.77 394.58,274.79 397.25,269.89C399.92,264.99 397.25,234.69 397.25,229.34C397.25,224 388.34,189.24 388.34,189.24L376.31,163.85L363.83,150.92z" />
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M125.4,316.07C131.29,322.5 137.59,328.57 144.31,334.13C151.03,339.69 158.19,344.71 165.74,349.05C173.28,353.4 181.24,357.04 189.49,359.81C197.74,362.59 206.28,364.49 214.92,365.49C223.56,366.49 232.29,366.59 240.96,365.89C249.63,365.19 258.23,363.7 266.67,361.57C283.53,357.33 299.83,350.58 314.22,340.82C321.41,335.94 328.09,330.33 334.12,324.08C340.15,317.83 345.51,310.94 350.17,303.61C359.49,288.93 365.97,272.55 370.2,255.68L370.59,254.1L370.78,253.32L370.94,252.52L371.61,249.31L371.94,247.71L372.02,247.31L372.04,247.21C372.04,247.24 372.05,247.16 372.05,247.14L372.08,246.94L372.2,246.13L372.68,242.89L372.92,241.27C372.99,240.8 373.03,240.2 373.08,239.68L373.38,236.42L373.53,234.82L373.6,233.18L373.73,229.91C373.97,221.22 373.31,212.52 372.05,203.9C370.71,195.31 368.33,186.93 365.5,178.7L364.41,175.62L364.13,174.85L364,174.48L363.85,174.1L363.24,172.59L362.01,169.55L361.4,168.06L360.7,166.58L359.32,163.62L358.99,162.91L358.58,162.2L357.77,160.78L356.16,157.94C344.68,130.92 196.21,368.72 125.31,316.16zM125.39,316.06C207.54,352.13 331.55,220.04 354.89,154.81C355.62,155.68 356.24,156.57 356.88,157.47C357.19,157.93 357.51,158.34 357.8,158.87L358.62,160.29L359.43,161.71L359.84,162.42C360,162.74 360.08,162.93 360.21,163.2L362.3,167.64C362.54,168.19 362.72,168.66 362.93,169.18L364.17,172.21L364.79,173.72L364.94,174.1L365.09,174.51L365.36,175.28L366.46,178.36C369.33,186.62 371.75,195.1 373.12,203.77C374.41,212.42 375.1,221.19 374.88,229.96L374.76,233.23L374.7,234.87L374.55,236.53L374.26,239.79C374.2,240.34 374.18,240.84 374.09,241.45L373.86,243.07L373.39,246.31L373.27,247.12L373.24,247.32L373.22,247.45L373.2,247.55L373.12,247.96L372.79,249.56L372.13,252.77L371.97,253.57L371.77,254.38L371.39,255.98C369.28,264.47 366.61,272.85 363.3,280.96C359.98,289.07 355.99,296.92 351.29,304.32C346.59,311.73 341.17,318.69 335.08,325C328.99,331.32 322.23,337 314.97,341.92C307.71,346.85 299.95,351.04 291.89,354.49C283.82,357.92 275.46,360.63 266.95,362.73C258.45,364.82 249.78,366.29 241.05,366.95C232.31,367.61 223.51,367.46 214.81,366.4C206.11,365.35 197.52,363.39 189.24,360.55C180.95,357.72 172.97,354.02 165.41,349.61C157.85,345.21 150.73,340.07 144.05,334.44C137.37,328.8 131.13,322.66 125.3,316.16z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="716.14"
|
||||||
|
android:endY="-34.42"
|
||||||
|
android:startX="466.51"
|
||||||
|
android:startY="-34.42"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#FFF15A24"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#00F15A24"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</group>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M126.46,317.13C142.59,329.89 167.8,329.79 195.46,322.35C223.13,314.9 253.27,300.13 278.96,280.51C308.08,258.28 328.84,239.6 343.99,214.28C350.83,202.85 355.3,192.39 357.66,182.52C360.02,172.66 359.09,164.4 355.37,155.3C355.37,155.3 370.14,176.62 374.15,209.98C376.15,226.66 375.12,249.78 366.6,272.52C358.67,293.7 348.01,313.54 329.84,329.83C329.15,330.45 321.66,338.19 307.77,346.16C290.62,356.01 276.36,361.24 257.16,364.68C233.76,368.87 217.32,367.66 199.65,363.47C174.99,357.61 151.55,344.34 125.79,317.21">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="239.68"
|
||||||
|
android:centerY="255.39"
|
||||||
|
android:gradientRadius="158.35"
|
||||||
|
android:type="radial">
|
||||||
|
<item
|
||||||
|
android:color="#00F15A24"
|
||||||
|
android:offset="0" />
|
||||||
|
<item
|
||||||
|
android:color="#00F15A24"
|
||||||
|
android:offset="0.69" />
|
||||||
|
<item
|
||||||
|
android:color="#FFF14A24"
|
||||||
|
android:offset="1" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</vector>
|
@@ -0,0 +1,51 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="192dp"
|
||||||
|
android:height="192dp"
|
||||||
|
android:viewportWidth="500"
|
||||||
|
android:viewportHeight="500">
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M237.24,238.74C235.5,247.54 235.3,256.45 235.32,266.13C235.42,270.41 235.32,275.43 235.86,280.46C236.98,290.68 240.45,293.03 249.78,288.93C265.7,281.95 279.81,272.16 292.42,260.21C295.98,256.84 299.72,253.59 302.3,249.33C304.69,245.41 304.17,243.2 300.29,240.77C299.04,239.99 297.66,239.4 296.27,238.9C291.66,237.22 286.88,236.12 282.09,235.08C270.84,232.66 259.61,230.05 248.04,229.89C247.96,229.89 247.89,229.89 247.82,229.89C242.07,229.89 238.35,233.1 237.24,238.74"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M222.5,240.44C219.61,241.88 217.31,244.14 215.05,246.4C199.92,261.46 185.65,277.32 171.68,293.46C167.88,297.85 163.97,302.22 161.54,307.62C159.83,311.42 160.71,313.37 164.8,314.45C169.01,315.56 173.3,316.04 177.66,315.66C188.09,314.75 197.98,311.81 207.65,307.93C216.6,304.34 222.21,297.97 224.18,288.47C226.75,276.01 227.66,263.36 228.29,250.69C228.29,248.75 228.31,246.81 228.28,244.86C228.27,244.29 228.15,243.73 228.04,243.17C227.55,240.74 226.75,239.59 225.27,239.59C224.53,239.59 223.62,239.88 222.5,240.44"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M339.84,178.38C337.27,178.84 334.73,179.56 332.26,180.41C318.25,185.3 304.95,191.86 291.47,197.97C279.95,203.19 268.45,208.47 257.53,214.91C255.38,216.18 252.76,217.68 253.28,220.55C253.73,223 256.63,223.03 258.64,223.5C274.1,227.13 289.84,228.59 304.37,229.79C313.71,229.69 321.24,228.2 327.11,221.93C333.24,215.37 338.37,208.12 342.19,200C344.58,194.91 346.18,189.5 346.18,183.81C346.18,180.11 344.54,178.22 341.48,178.22C340.97,178.22 340.43,178.28 339.84,178.38"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M208.07,237.99C207.75,238.09 207.44,238.25 207.13,238.38C187.03,246.55 167.73,256.43 148.43,266.29C143.56,268.78 138.86,271.58 134.46,274.89C129.74,278.44 127.05,282.9 127.64,288.88C127.64,289.67 127.61,290.36 127.64,291.04C128,297.84 131.43,302.61 137.28,305.8C142.71,308.76 147.39,306.84 151.86,303.55C152.96,302.74 154.09,301.95 155.11,301.05C175.47,283.13 193.68,263.12 211.56,242.79C212.69,241.51 214.63,239.99 213.35,238.07C212.81,237.24 212.17,236.97 211.48,236.97C210.38,236.97 209.17,237.68 208.07,237.99"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M309.87,147.88C307.95,149.3 306.15,150.89 304.35,152.47C291.58,163.74 280.22,176.44 268.48,188.74C263.6,193.85 258.79,199.07 254.56,204.76C253.83,205.76 252.91,206.87 253.82,208.17C254.69,209.4 256.07,209.19 257.27,208.83C258.9,208.33 260.5,207.69 262.07,207.03C283.2,198.1 303.64,187.73 324,177.21C329.85,174.18 335.75,171.1 340.56,166.4C342.36,164.64 343.48,162.62 343.54,159.77C343.48,157.72 342.7,155.59 341.56,153.57C337.94,147.16 330.59,143.63 323.11,143.63C318.46,143.63 313.77,144.99 309.87,147.88"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M152.4,226.49C147.18,232.43 142.24,238.54 138.22,245.36C135.66,249.7 133.61,254.32 132.56,259.29C131.83,262.73 132.92,264.15 135.86,264.2C137.72,264.18 139.46,263.66 141.18,263.04C158.06,257 174.12,249.06 190.35,241.5C197.27,238.28 204.15,234.93 210.65,230.89C212.6,229.67 214.78,228.22 214.38,225.62C214.03,223.3 211.39,223.55 209.7,222.79C209.08,222.52 208.39,222.4 207.72,222.25C197.27,219.78 186.62,218.65 175.98,217.52C174.75,217.39 173.54,217.32 172.36,217.32C164.6,217.32 157.94,220.18 152.4,226.49"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M289.75,141.89C277.12,143.2 265.34,147.29 253.93,152.69C247.04,155.94 242.73,161.14 241.32,168.68C239.72,177.24 238.61,185.84 238.73,194.57C238.73,196.97 238.67,199.37 238.75,201.77C238.81,203.55 238.4,205.69 240.38,206.68C242.36,207.67 244.26,206.6 245.78,205.44C248.31,203.5 250.74,201.4 253,199.15C265.53,186.65 277.31,173.45 289.02,160.19C292.63,156.1 296.43,152.08 298.79,147.02C300.21,143.97 299.51,142.59 296.22,141.94C295.23,141.75 294.23,141.68 293.23,141.68C292.07,141.68 290.91,141.78 289.75,141.89"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M223.76,167.89C220.24,168.99 217.12,170.86 213.95,172.68C200.94,180.13 188.86,188.83 178.5,199.77C173.7,204.85 174.46,208.17 181.12,210.07C193.5,213.61 206.07,216.34 219.01,216.66C225.82,216.83 229.25,213.65 230.47,207.01C231.71,200.22 231.97,193.4 231.79,184.38C231.6,181.64 232.49,176.67 231.18,171.86C230.34,168.76 228.93,167.38 226.6,167.38C225.78,167.38 224.83,167.56 223.76,167.89"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M356.04,157.69C356.25,158.22 356.29,158.38 356.37,158.63C356.53,159.1 356.62,159.36 356.7,159.61C356.78,159.86 356.72,159.72 356.8,159.96C356.87,160.21 356.96,160.48 357.03,160.72C357.1,160.97 357.17,161.31 357.23,161.55C357.3,161.8 357.38,162.09 357.44,162.34C357.5,162.58 357.67,163.53 357.72,163.78C358,165.2 357.93,165.67 358.07,167.08C358.22,168.48 358.29,169.88 358.31,171.27C358.32,172.67 358.27,174.05 358.17,175.43C358.06,176.8 357.91,178.17 357.69,179.54C357.48,180.9 357.22,182.25 356.91,183.6C356.61,184.95 356.25,186.29 355.86,187.63C355.46,188.96 355.03,190.29 354.56,191.62C354.09,192.94 353.58,194.26 353.05,195.57C352,198.15 350.87,200.68 349.66,203.15C348.45,205.63 347.17,208.06 345.83,210.43C344.48,212.81 343.06,215.15 341.59,217.44C340.11,219.73 338.58,221.97 336.99,224.19C335.4,226.4 333.75,228.57 332.06,230.71C330.36,232.85 328.62,234.95 326.84,237.02C325.06,239.1 323.24,241.14 321.38,243.16C319.52,245.18 317.63,247.18 315.71,249.15C313.74,251.18 311.73,253.18 309.69,255.14C307.65,257.09 305.58,259.01 303.49,260.9C301.39,262.79 299.26,264.64 297.11,266.46C294.96,268.28 292.78,270.06 290.58,271.82C288.37,273.57 286.14,275.3 283.89,276.99C281.64,278.69 279.36,280.35 277.06,281.99C274.76,283.63 272.44,285.24 270.1,286.82C267.75,288.41 265.39,289.97 263.01,291.5C260.04,293.41 257.06,295.26 254.04,297.05C251.03,298.83 247.99,300.56 244.93,302.23C241.86,303.89 238.77,305.49 235.64,307.02C232.52,308.55 229.37,310.02 226.19,311.41C223.01,312.8 219.79,314.12 216.55,315.37C214.9,316 213.25,316.62 211.59,317.21C209.97,317.79 208.35,318.34 206.72,318.88C203.4,319.98 200.06,321 196.68,321.93C193.3,322.87 189.88,323.72 186.43,324.49C184.98,324.81 183.53,325.11 182.08,325.38C180.64,325.65 179.18,325.9 177.73,326.12C176.28,326.33 174.82,326.52 173.37,326.68C171.91,326.85 170.46,326.98 169,327.08C167.54,327.18 166.08,327.26 164.62,327.3C163.16,327.34 161.69,327.35 160.23,327.33C158.77,327.31 157.3,327.25 155.84,327.17C154.37,327.08 152.9,326.96 151.44,326.8C150.25,326.68 149.06,326.52 147.89,326.33C146.72,326.14 145.56,325.91 144.41,325.65C143.26,325.39 142.13,325.09 141.01,324.76C139.89,324.42 138.78,324.04 137.7,323.62C136.61,323.19 135.54,322.73 134.49,322.22C133.82,321.89 133.16,321.55 132.51,321.18C132.04,320.93 131.58,320.66 131.13,320.39C130.12,319.78 129.13,319.12 128.16,318.41C127.5,317.92 126.85,317.41 126.21,316.87C125.96,316.67 125.71,316.47 125.49,316.28C130.1,321.36 134.85,326.09 139.73,330.45C144.67,334.86 149.74,338.9 154.95,342.55C160.15,346.2 165.5,349.45 170.97,352.31C176.45,355.17 182.07,357.62 187.82,359.65C193.57,361.68 199.45,363.3 205.47,364.47C211.5,365.66 217.65,366.4 223.95,366.7C230.24,366.99 236.67,366.84 243.24,366.23C249.8,365.61 256.5,364.53 263.34,362.97C270.63,361.31 277.58,359.26 284.17,356.84C290.76,354.41 297,351.61 302.89,348.45C308.78,345.28 314.32,341.75 319.52,337.85C324.71,333.96 329.55,329.71 334.05,325.1C338.54,320.5 342.69,315.54 346.49,310.24C350.29,304.94 353.74,299.3 356.85,293.32C359.95,287.35 362.72,281.04 365.13,274.4C367.55,267.76 369.62,260.8 371.35,253.51C371.83,251.46 372.26,249.39 372.62,247.33C372.99,245.26 373.29,243.19 373.53,241.12C373.78,239.04 373.97,236.97 374.1,234.89C374.23,232.81 374.32,230.73 374.35,228.64C374.38,226.56 374.36,224.47 374.29,222.38C374.23,220.29 374.11,218.2 373.96,216.11C373.8,214.02 373.61,211.93 373.37,209.83C373.13,207.74 372.85,205.65 372.54,203.55C372.19,201.22 371.73,198.79 371.17,196.31C370.62,193.83 369.97,191.31 369.26,188.8C368.56,186.29 367.78,183.79 366.97,181.35C366.17,178.92 365.32,176.55 364.47,174.3C363.61,172.05 362.74,169.93 361.9,167.97C361.05,166.02 360.21,164.25 359.43,162.71C358.64,161.17 357.89,159.86 357.21,158.84C356.79,158.2 356.1,157.21 355.48,156.36C355.67,156.78 355.87,157.23 356.04,157.69"
|
||||||
|
android:fillColor="@android:color/white">
|
||||||
|
</path>
|
||||||
|
</vector>
|
@@ -2,6 +2,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
@@ -17,14 +18,12 @@
|
|||||||
android:id="@+id/appbar_cheats"
|
android:id="@+id/appbar_cheats"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:elevation="0dp"
|
android:fitsSystemWindows="true">
|
||||||
app:liftOnScroll="false">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar_cheats"
|
android:id="@+id/toolbar_cheats"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="?attr/actionBarSize" />
|
||||||
android:background="?attr/colorSurface" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
@@ -40,18 +39,20 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/coordinator_cheats">
|
app:layout_constraintTop_toBottomOf="@id/coordinator_cheats">
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:layout_width="320dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:id="@+id/cheat_list"
|
android:id="@+id/cheat_list_container"
|
||||||
android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment" />
|
android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment"
|
||||||
|
tools:layout="@layout/fragment_cheat_list" />
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:layout_width="320dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:id="@+id/cheat_details"
|
android:id="@+id/cheat_details_container"
|
||||||
android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment" />
|
android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment"
|
||||||
|
tools:layout="@layout/fragment_cheat_details" />
|
||||||
|
|
||||||
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
|
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
|
||||||
|
|
||||||
|
@@ -9,13 +9,14 @@
|
|||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appbar"
|
android:id="@+id/appbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
app:liftOnScrollTargetViewId="@id/grid_games">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar_main"
|
android:id="@+id/toolbar_main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:background="?attr/colorSurface"
|
|
||||||
app:subtitleTextColor="?attr/colorOnSurface"
|
app:subtitleTextColor="?attr/colorOnSurface"
|
||||||
app:titleTextColor="?attr/colorOnSurface" />
|
app:titleTextColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
@@ -8,13 +8,13 @@
|
|||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appbar_settings"
|
android:id="@+id/appbar_settings"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar_settings"
|
android:id="@+id/toolbar_settings"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize" />
|
||||||
android:background="?attr/colorSurface" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textSize="@dimen/text_medium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_medlarge"
|
||||||
|
android:layout_marginLeft="@dimen/spacing_large"
|
||||||
|
android:layout_marginRight="@dimen/spacing_large"
|
||||||
|
android:text="@string/select_citra_user_folder" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/path"
|
||||||
|
android:textSize="@dimen/text_medium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_small"
|
||||||
|
android:layout_marginLeft="@dimen/spacing_large"
|
||||||
|
android:layout_marginRight="@dimen/spacing_large"
|
||||||
|
android:text="@string/fatal_error" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/space"
|
||||||
|
android:textSize="@dimen/text_medium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_small"
|
||||||
|
android:layout_marginLeft="@dimen/spacing_large"
|
||||||
|
android:layout_marginRight="@dimen/spacing_large"
|
||||||
|
android:text="@string/free_space" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/checkBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="@dimen/spacing_small"
|
||||||
|
android:layout_marginLeft="@dimen/spacing_large"
|
||||||
|
android:layout_marginRight="@dimen/spacing_large"
|
||||||
|
android:text="@string/move_data" />
|
||||||
|
</LinearLayout>
|
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/nnf_picker_toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:background="?attr/colorPrimary"
|
|
||||||
android:minHeight="?attr/actionBarSize"
|
|
||||||
android:theme="?nnf_toolbarTheme">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/filepicker_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="start"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/nnf_current_dir"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="start"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle" />
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.appcompat.widget.Toolbar>
|
|
@@ -9,6 +9,7 @@
|
|||||||
android:id="@+id/cheat_list"
|
android:id="@+id/cheat_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
android:src="@drawable/ic_add"
|
android:src="@drawable/ic_add"
|
||||||
android:contentDescription="@string/cheats_add"
|
android:contentDescription="@string/cheats_add"
|
||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@@ -26,6 +26,7 @@
|
|||||||
android:id="@+id/grid_games"
|
android:id="@+id/grid_games"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
tools:listitem="@layout/card_game" />
|
tools:listitem="@layout/card_game" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<org.citra.citra_emu.features.settings.ui.SettingsFrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/list_settings_root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface">
|
android:background="?attr/colorSurface">
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/list_settings"
|
android:id="@+id/list_settings"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false" />
|
||||||
|
|
||||||
</org.citra.citra_emu.features.settings.ui.SettingsFrameLayout>
|
</FrameLayout>
|
||||||
|
@@ -24,11 +24,12 @@
|
|||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/checkbox"
|
android:id="@+id/checkbox"
|
||||||
android:layout_width="48dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="64dp"
|
android:layout_height="wrap_content"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:nextFocusLeft="@id/root"
|
android:nextFocusLeft="@id/root"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/text_name"
|
app:layout_constraintStart_toEndOf="@id/text_name"
|
||||||
|
@@ -13,6 +13,11 @@
|
|||||||
android:title="@string/select_game_folder"
|
android:title="@string/select_game_folder"
|
||||||
app:showAsAction="ifRoom">
|
app:showAsAction="ifRoom">
|
||||||
<menu>
|
<menu>
|
||||||
|
<item
|
||||||
|
android:id="@+id/button_select_root"
|
||||||
|
android:icon="@drawable/ic_folder"
|
||||||
|
android:title="@string/select_citra_user_folder"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/button_add_directory"
|
android:id="@+id/button_add_directory"
|
||||||
android:icon="@drawable/ic_folder"
|
android:icon="@drawable/ic_folder"
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_citra" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_citra_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 24 KiB |
@@ -98,7 +98,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">Spieleordner auswählen</string>
|
<string name="select_game_folder">Spieleordner auswählen</string>
|
||||||
<string name="add_directory_title">Ordner zur Bibliothek hinzufügen</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">Einstellungen</string>
|
<string name="preferences_settings">Einstellungen</string>
|
||||||
|
@@ -102,7 +102,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">Seleccionar Directorio de Juego</string>
|
<string name="select_game_folder">Seleccionar Directorio de Juego</string>
|
||||||
<string name="add_directory_title">Añadir Carpeta a la Librería de Juegos</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">Configuración</string>
|
<string name="preferences_settings">Configuración</string>
|
||||||
|
@@ -63,7 +63,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">Valitse pelikansio</string>
|
<string name="select_game_folder">Valitse pelikansio</string>
|
||||||
<string name="add_directory_title">Lisää kansio kirjastoosi</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">Asetukset</string>
|
<string name="preferences_settings">Asetukset</string>
|
||||||
|
@@ -98,7 +98,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">Choisir un répertoire de jeu</string>
|
<string name="select_game_folder">Choisir un répertoire de jeu</string>
|
||||||
<string name="add_directory_title">Ajouter un répertoire à la bibliothèque</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">Paramètres</string>
|
<string name="preferences_settings">Paramètres</string>
|
||||||
|
@@ -98,7 +98,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">Seleziona Cartella di Gioco</string>
|
<string name="select_game_folder">Seleziona Cartella di Gioco</string>
|
||||||
<string name="add_directory_title">Aggiungi una Cartella alla Libreria</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">Impostazioni</string>
|
<string name="preferences_settings">Impostazioni</string>
|
||||||
|
@@ -66,7 +66,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">ゲームフォルダを選択</string>
|
<string name="select_game_folder">ゲームフォルダを選択</string>
|
||||||
<string name="add_directory_title">ライブラリにフォルダを追加</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">設定</string>
|
<string name="preferences_settings">設定</string>
|
||||||
|
@@ -100,7 +100,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">게임 폴더 선택</string>
|
<string name="select_game_folder">게임 폴더 선택</string>
|
||||||
<string name="add_directory_title">폴더를 라이브러리에 추가</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">설정</string>
|
<string name="preferences_settings">설정</string>
|
||||||
|
@@ -98,7 +98,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">Velg Spill Mappe</string>
|
<string name="select_game_folder">Velg Spill Mappe</string>
|
||||||
<string name="add_directory_title">Lett til Mappe til Bibliotek </string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">Innstillinger </string>
|
<string name="preferences_settings">Innstillinger </string>
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="FilePickerBaseTheme" parent="NNF_BaseTheme" />
|
|
||||||
</resources>
|
|
@@ -98,7 +98,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">Escolher pasta de jogos</string>
|
<string name="select_game_folder">Escolher pasta de jogos</string>
|
||||||
<string name="add_directory_title">Adicionar pasta à biblioteca</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">Configurações</string>
|
<string name="preferences_settings">Configurações</string>
|
||||||
|
@@ -98,7 +98,6 @@
|
|||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
<string name="select_game_folder">选择游戏目录</string>
|
<string name="select_game_folder">选择游戏目录</string>
|
||||||
<string name="add_directory_title">添加文件夹到库中</string>
|
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
<string name="preferences_settings">设置</string>
|
<string name="preferences_settings">设置</string>
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="text_medium">18dp</dimen>
|
||||||
|
|
||||||
<dimen name="spacing_small">4dp</dimen>
|
<dimen name="spacing_small">4dp</dimen>
|
||||||
<dimen name="spacing_medlarge">12dp</dimen>
|
<dimen name="spacing_medlarge">12dp</dimen>
|
||||||
<dimen name="spacing_large">16dp</dimen>
|
<dimen name="spacing_large">16dp</dimen>
|
||||||
|
<dimen name="spacing_fab_list">80dp</dimen>
|
||||||
|
|
||||||
<dimen name="dialog_margin">20dp</dimen>
|
<dimen name="dialog_margin">20dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -3,11 +3,12 @@
|
|||||||
|
|
||||||
<!-- General application strings -->
|
<!-- General application strings -->
|
||||||
<string name="app_name" translatable="false">Citra</string>
|
<string name="app_name" translatable="false">Citra</string>
|
||||||
<string name="app_disclaimer">This software will run games for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you run, please place your rightfully owned 3DS game files onto your device storage.</string>
|
<string name="app_disclaimer">This software will run games for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you can begin with emulating, please select a folder to store Citra\'s user data in.\n\nWhat\'s this:\n<a href='https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'>Wiki - Citra Android user data and storage</a></string>
|
||||||
<string name="app_notification_channel_name" translatable="false">Citra</string>
|
<string name="app_notification_channel_name" translatable="false">Citra</string>
|
||||||
<string name="app_notification_channel_id" translatable="false">Citra</string>
|
<string name="app_notification_channel_id" translatable="false">Citra</string>
|
||||||
<string name="app_notification_channel_description">Citra 3DS emulator notifications</string>
|
<string name="app_notification_channel_description">Citra 3DS emulator notifications</string>
|
||||||
<string name="app_notification_running">Citra is running</string>
|
<string name="app_notification_running">Citra is running</string>
|
||||||
|
<string name="app_game_install_description">Next, you will need to select a Game Folder. Citra will display all of the 3DS ROMs inside of the selected folder in the app.\n\nCIA ROMs, updates and DLC will need to be installed separately by clicking on the folder icon and selecting Install CIA.</string>
|
||||||
|
|
||||||
<!-- Input related strings -->
|
<!-- Input related strings -->
|
||||||
<string name="controller_circlepad">Circle Pad</string>
|
<string name="controller_circlepad">Circle Pad</string>
|
||||||
@@ -145,6 +146,7 @@
|
|||||||
<string name="grid_menu_core_settings">Settings</string>
|
<string name="grid_menu_core_settings">Settings</string>
|
||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- Add Directory Screen-->
|
||||||
|
<string name="select_citra_user_folder">Select Citra User Folder</string>
|
||||||
<string name="select_game_folder">Select Game Folder</string>
|
<string name="select_game_folder">Select Game Folder</string>
|
||||||
<string name="install_cia_title">Install CIA</string>
|
<string name="install_cia_title">Install CIA</string>
|
||||||
|
|
||||||
@@ -205,6 +207,10 @@
|
|||||||
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
||||||
|
|
||||||
<string name="do_not_show_this_again">Do not show this again</string>
|
<string name="do_not_show_this_again">Do not show this again</string>
|
||||||
|
<string name="searching_direcotry">Searching directory: %s</string>
|
||||||
|
<string name="move_data">Move Data</string>
|
||||||
|
<string name="copy_file_name">Copy file: %s</string>
|
||||||
|
<string name="free_space">Free space: %.2f GB</string>
|
||||||
<string name="savestate_warning_title">Savestates</string>
|
<string name="savestate_warning_title">Savestates</string>
|
||||||
<string name="savestate_warning_message">Warning: Savestates are NOT a replacement for in-game saves, and are not meant to be reliable.\n\nUse at your own risk!</string>
|
<string name="savestate_warning_message">Warning: Savestates are NOT a replacement for in-game saves, and are not meant to be reliable.\n\nUse at your own risk!</string>
|
||||||
|
|
||||||
@@ -234,6 +240,7 @@
|
|||||||
<string name="continue_button">Continue</string>
|
<string name="continue_button">Continue</string>
|
||||||
<string name="system_archive_not_found">System Archive Not Found</string>
|
<string name="system_archive_not_found">System Archive Not Found</string>
|
||||||
<string name="system_archive_not_found_message">%s is missing. Please dump your system archives.\nContinuing emulation may result in crashes and bugs.</string>
|
<string name="system_archive_not_found_message">%s is missing. Please dump your system archives.\nContinuing emulation may result in crashes and bugs.</string>
|
||||||
|
<string name="cia_file_not_found">Installation Failed. CIA file not found.</string>
|
||||||
<string name="system_archive_general">A system archive</string>
|
<string name="system_archive_general">A system archive</string>
|
||||||
<string name="save_load_error">Save/Load Error</string>
|
<string name="save_load_error">Save/Load Error</string>
|
||||||
<string name="fatal_error">Fatal Error</string>
|
<string name="fatal_error">Fatal Error</string>
|
||||||
|
@@ -1,12 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert">
|
|
||||||
<item name="colorPrimary">@color/citra_primary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/citra_secondary</item>
|
|
||||||
<item name="colorAccent">@color/citra_primaryContainer</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="CitraMaterialDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
<style name="CitraMaterialDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
||||||
<item name="colorPrimary">@color/citra_surface</item>
|
<item name="colorPrimary">@color/citra_surface</item>
|
||||||
<item name="colorSurface">@color/citra_surface</item>
|
<item name="colorSurface">@color/citra_surface</item>
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="FilePickerBaseTheme" parent="NNF_BaseTheme.Light" />
|
|
||||||
</resources>
|
|
@@ -1,6 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.Citra.Splash.Main" parent="Theme.SplashScreen">
|
||||||
|
<item name="windowSplashScreenBackground">@color/citra_surface</item>
|
||||||
|
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_citra</item>
|
||||||
|
<item name="postSplashScreenTheme">@style/Theme.Citra.Main</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="Theme.Citra.Main" parent="Theme.Material3.DayNight.NoActionBar">
|
<style name="Theme.Citra.Main" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
<!-- Main theme colors -->
|
<!-- Main theme colors -->
|
||||||
<item name="colorPrimary">@color/citra_primary</item>
|
<item name="colorPrimary">@color/citra_primary</item>
|
||||||
@@ -31,7 +38,8 @@
|
|||||||
<item name="colorPrimaryInverse">@color/citra_inversePrimary</item>
|
<item name="colorPrimaryInverse">@color/citra_inversePrimary</item>
|
||||||
|
|
||||||
<item name="homeAsUpIndicator">@drawable/ic_back</item>
|
<item name="homeAsUpIndicator">@drawable/ic_back</item>
|
||||||
<item name="android:statusBarColor">@color/citra_surface</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="android:textColorLink">@color/citra_primary</item>
|
<item name="android:textColorLink">@color/citra_primary</item>
|
||||||
<item name="materialAlertDialogTheme">@style/CitraMaterialDialog</item>
|
<item name="materialAlertDialogTheme">@style/CitraMaterialDialog</item>
|
||||||
<item name="popupMenuStyle">@style/CitraShapedPopup</item>
|
<item name="popupMenuStyle">@style/CitraShapedPopup</item>
|
||||||
@@ -39,17 +47,4 @@
|
|||||||
<item name="sliderStyle">@style/CitraSlider</item>
|
<item name="sliderStyle">@style/CitraSlider</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Inherit from a base file picker theme that handles day/night -->
|
|
||||||
<style name="FilePickerTheme" parent="FilePickerBaseTheme">
|
|
||||||
<item name="colorPrimary">@color/citra_primary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/citra_primary</item>
|
|
||||||
<item name="colorAccent">@color/citra_primary</item>
|
|
||||||
|
|
||||||
<!-- Need to set this also to style create folder dialog -->
|
|
||||||
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
|
|
||||||
|
|
||||||
<item name="nnf_list_item_divider">@drawable/gamelist_divider</item>
|
|
||||||
<item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@@ -140,15 +140,10 @@ void Config::ReadValues() {
|
|||||||
sdl2_config->GetInteger("Renderer", "render_3d", 0));
|
sdl2_config->GetInteger("Renderer", "render_3d", 0));
|
||||||
Settings::values.factor_3d =
|
Settings::values.factor_3d =
|
||||||
static_cast<u8>(sdl2_config->GetInteger("Renderer", "factor_3d", 0));
|
static_cast<u8>(sdl2_config->GetInteger("Renderer", "factor_3d", 0));
|
||||||
std::string default_shader = "none (builtin)";
|
|
||||||
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Anaglyph)
|
|
||||||
default_shader = "dubois (builtin)";
|
|
||||||
else if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Interlaced ||
|
|
||||||
Settings::values.render_3d.GetValue() ==
|
|
||||||
Settings::StereoRenderOption::ReverseInterlaced)
|
|
||||||
default_shader = "horizontal (builtin)";
|
|
||||||
Settings::values.pp_shader_name =
|
Settings::values.pp_shader_name =
|
||||||
sdl2_config->GetString("Renderer", "pp_shader_name", default_shader);
|
sdl2_config->GetString("Renderer", "pp_shader_name", "none (builtin)");
|
||||||
|
Settings::values.anaglyph_shader_name =
|
||||||
|
sdl2_config->GetString("Renderer", "anaglyph_shader_name", "dubois (builtin)");
|
||||||
Settings::values.filter_mode = sdl2_config->GetBoolean("Renderer", "filter_mode", true);
|
Settings::values.filter_mode = sdl2_config->GetBoolean("Renderer", "filter_mode", true);
|
||||||
|
|
||||||
Settings::values.bg_red = static_cast<float>(sdl2_config->GetReal("Renderer", "bg_red", 0.0));
|
Settings::values.bg_red = static_cast<float>(sdl2_config->GetReal("Renderer", "bg_red", 0.0));
|
||||||
|
@@ -172,9 +172,12 @@ mono_render_option =
|
|||||||
|
|
||||||
# The name of the post processing shader to apply.
|
# The name of the post processing shader to apply.
|
||||||
# Loaded from shaders if render_3d is off or side by side.
|
# Loaded from shaders if render_3d is off or side by side.
|
||||||
# Loaded from shaders/anaglyph if render_3d is anaglyph
|
|
||||||
pp_shader_name =
|
pp_shader_name =
|
||||||
|
|
||||||
|
# The name of the shader to apply when render_3d is anaglyph.
|
||||||
|
# Loaded from shaders/anaglyph
|
||||||
|
anaglyph_shader_name =
|
||||||
|
|
||||||
# Whether to enable linear filtering or not
|
# Whether to enable linear filtering or not
|
||||||
# This is required for some shaders to work correctly
|
# This is required for some shaders to work correctly
|
||||||
# 0: Nearest, 1 (default): Linear
|
# 0: Nearest, 1 (default): Linear
|
||||||
|
@@ -271,6 +271,10 @@ if (NOT WIN32)
|
|||||||
target_include_directories(citra-qt PRIVATE ${Qt5Gui_PRIVATE_INCLUDE_DIRS})
|
target_include_directories(citra-qt PRIVATE ${Qt5Gui_PRIVATE_INCLUDE_DIRS})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (UNIX AND NOT APPLE)
|
||||||
|
target_link_libraries(citra-qt PRIVATE Qt5::DBus)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_compile_definitions(citra-qt PRIVATE
|
target_compile_definitions(citra-qt PRIVATE
|
||||||
# Use QStringBuilder for string concatenation to reduce
|
# Use QStringBuilder for string concatenation to reduce
|
||||||
# the overall number of temporary strings created.
|
# the overall number of temporary strings created.
|
||||||
|
@@ -149,14 +149,14 @@ void Config::ReadGlobalSetting(Settings::SwitchableSetting<Type, ranged>& settin
|
|||||||
const bool use_global = qt_config->value(name + QStringLiteral("/use_global"), true).toBool();
|
const bool use_global = qt_config->value(name + QStringLiteral("/use_global"), true).toBool();
|
||||||
setting.SetGlobal(use_global);
|
setting.SetGlobal(use_global);
|
||||||
if (global || !use_global) {
|
if (global || !use_global) {
|
||||||
QVariant value{};
|
QVariant default_value{};
|
||||||
if constexpr (std::is_enum_v<Type>) {
|
if constexpr (std::is_enum_v<Type>) {
|
||||||
using TypeU = std::underlying_type_t<Type>;
|
using TypeU = std::underlying_type_t<Type>;
|
||||||
value = QVariant::fromValue<TypeU>(static_cast<TypeU>(setting.GetDefault()));
|
default_value = QVariant::fromValue<TypeU>(static_cast<TypeU>(setting.GetDefault()));
|
||||||
setting.SetValue(static_cast<Type>(ReadSetting(name, value).value<TypeU>()));
|
setting.SetValue(static_cast<Type>(ReadSetting(name, default_value).value<TypeU>()));
|
||||||
} else {
|
} else {
|
||||||
value = QVariant::fromValue<Type>(setting.GetDefault());
|
default_value = QVariant::fromValue<Type>(setting.GetDefault());
|
||||||
setting.SetValue(ReadSetting(name, value).value<Type>());
|
setting.SetValue(ReadSetting(name, default_value).value<Type>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +182,15 @@ void Config::WriteBasicSetting(const Settings::Setting<std::string>& setting) {
|
|||||||
qt_config->setValue(name, QString::fromStdString(value));
|
qt_config->setValue(name, QString::fromStdString(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicit u16 definition: Qt would store it as QMetaType otherwise, which is not human-readable
|
||||||
|
template <>
|
||||||
|
void Config::WriteBasicSetting(const Settings::Setting<u16>& setting) {
|
||||||
|
const QString name = QString::fromStdString(setting.GetLabel());
|
||||||
|
const u16& value = setting.GetValue();
|
||||||
|
qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault());
|
||||||
|
qt_config->setValue(name, static_cast<u32>(value));
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Type, bool ranged>
|
template <typename Type, bool ranged>
|
||||||
void Config::WriteBasicSetting(const Settings::Setting<Type, ranged>& setting) {
|
void Config::WriteBasicSetting(const Settings::Setting<Type, ranged>& setting) {
|
||||||
const QString name = QString::fromStdString(setting.GetLabel());
|
const QString name = QString::fromStdString(setting.GetLabel());
|
||||||
@@ -224,6 +233,20 @@ void Config::WriteGlobalSetting(const Settings::SwitchableSetting<std::string>&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicit u16 definition: Qt would store it as QMetaType otherwise, which is not human-readable
|
||||||
|
template <>
|
||||||
|
void Config::WriteGlobalSetting(const Settings::SwitchableSetting<u16, true>& setting) {
|
||||||
|
const QString name = QString::fromStdString(setting.GetLabel());
|
||||||
|
const u16& value = setting.GetValue(global);
|
||||||
|
if (!global) {
|
||||||
|
qt_config->setValue(name + QStringLiteral("/use_global"), setting.UsingGlobal());
|
||||||
|
}
|
||||||
|
if (global || !setting.UsingGlobal()) {
|
||||||
|
qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault());
|
||||||
|
qt_config->setValue(name, static_cast<u32>(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Config::ReadValues() {
|
void Config::ReadValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
ReadControlValues();
|
ReadControlValues();
|
||||||
@@ -233,7 +256,6 @@ void Config::ReadValues() {
|
|||||||
ReadDebuggingValues();
|
ReadDebuggingValues();
|
||||||
ReadWebServiceValues();
|
ReadWebServiceValues();
|
||||||
ReadVideoDumpingValues();
|
ReadVideoDumpingValues();
|
||||||
ReadUtilityValues();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadUIValues();
|
ReadUIValues();
|
||||||
@@ -242,6 +264,7 @@ void Config::ReadValues() {
|
|||||||
ReadLayoutValues();
|
ReadLayoutValues();
|
||||||
ReadAudioValues();
|
ReadAudioValues();
|
||||||
ReadSystemValues();
|
ReadSystemValues();
|
||||||
|
ReadUtilityValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::ReadAudioValues() {
|
void Config::ReadAudioValues() {
|
||||||
@@ -413,9 +436,9 @@ void Config::ReadControlValues() {
|
|||||||
void Config::ReadUtilityValues() {
|
void Config::ReadUtilityValues() {
|
||||||
qt_config->beginGroup(QStringLiteral("Utility"));
|
qt_config->beginGroup(QStringLiteral("Utility"));
|
||||||
|
|
||||||
ReadBasicSetting(Settings::values.dump_textures);
|
ReadGlobalSetting(Settings::values.dump_textures);
|
||||||
ReadBasicSetting(Settings::values.custom_textures);
|
ReadGlobalSetting(Settings::values.custom_textures);
|
||||||
ReadBasicSetting(Settings::values.preload_textures);
|
ReadGlobalSetting(Settings::values.preload_textures);
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
@@ -476,14 +499,9 @@ void Config::ReadLayoutValues() {
|
|||||||
|
|
||||||
ReadGlobalSetting(Settings::values.render_3d);
|
ReadGlobalSetting(Settings::values.render_3d);
|
||||||
ReadGlobalSetting(Settings::values.factor_3d);
|
ReadGlobalSetting(Settings::values.factor_3d);
|
||||||
Settings::values.pp_shader_name =
|
|
||||||
ReadSetting(QStringLiteral("pp_shader_name"), (Settings::values.render_3d.GetValue() ==
|
|
||||||
Settings::StereoRenderOption::Anaglyph)
|
|
||||||
? QStringLiteral("dubois (builtin)")
|
|
||||||
: QStringLiteral("none (builtin)"))
|
|
||||||
.toString()
|
|
||||||
.toStdString();
|
|
||||||
ReadGlobalSetting(Settings::values.filter_mode);
|
ReadGlobalSetting(Settings::values.filter_mode);
|
||||||
|
ReadGlobalSetting(Settings::values.pp_shader_name);
|
||||||
|
ReadGlobalSetting(Settings::values.anaglyph_shader_name);
|
||||||
ReadGlobalSetting(Settings::values.layout_option);
|
ReadGlobalSetting(Settings::values.layout_option);
|
||||||
ReadGlobalSetting(Settings::values.swap_screen);
|
ReadGlobalSetting(Settings::values.swap_screen);
|
||||||
ReadGlobalSetting(Settings::values.upright_screen);
|
ReadGlobalSetting(Settings::values.upright_screen);
|
||||||
@@ -818,7 +836,6 @@ void Config::SaveValues() {
|
|||||||
SaveDebuggingValues();
|
SaveDebuggingValues();
|
||||||
SaveWebServiceValues();
|
SaveWebServiceValues();
|
||||||
SaveVideoDumpingValues();
|
SaveVideoDumpingValues();
|
||||||
SaveUtilityValues();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SaveUIValues();
|
SaveUIValues();
|
||||||
@@ -827,6 +844,7 @@ void Config::SaveValues() {
|
|||||||
SaveLayoutValues();
|
SaveLayoutValues();
|
||||||
SaveAudioValues();
|
SaveAudioValues();
|
||||||
SaveSystemValues();
|
SaveSystemValues();
|
||||||
|
SaveUtilityValues();
|
||||||
qt_config->sync();
|
qt_config->sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,9 +957,9 @@ void Config::SaveControlValues() {
|
|||||||
void Config::SaveUtilityValues() {
|
void Config::SaveUtilityValues() {
|
||||||
qt_config->beginGroup(QStringLiteral("Utility"));
|
qt_config->beginGroup(QStringLiteral("Utility"));
|
||||||
|
|
||||||
WriteBasicSetting(Settings::values.dump_textures);
|
WriteGlobalSetting(Settings::values.dump_textures);
|
||||||
WriteBasicSetting(Settings::values.custom_textures);
|
WriteGlobalSetting(Settings::values.custom_textures);
|
||||||
WriteBasicSetting(Settings::values.preload_textures);
|
WriteGlobalSetting(Settings::values.preload_textures);
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
@@ -997,12 +1015,9 @@ void Config::SaveLayoutValues() {
|
|||||||
|
|
||||||
WriteGlobalSetting(Settings::values.render_3d);
|
WriteGlobalSetting(Settings::values.render_3d);
|
||||||
WriteGlobalSetting(Settings::values.factor_3d);
|
WriteGlobalSetting(Settings::values.factor_3d);
|
||||||
WriteSetting(QStringLiteral("pp_shader_name"),
|
|
||||||
QString::fromStdString(Settings::values.pp_shader_name.GetValue()),
|
|
||||||
(Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Anaglyph)
|
|
||||||
? QStringLiteral("dubois (builtin)")
|
|
||||||
: QStringLiteral("none (builtin)"));
|
|
||||||
WriteGlobalSetting(Settings::values.filter_mode);
|
WriteGlobalSetting(Settings::values.filter_mode);
|
||||||
|
WriteGlobalSetting(Settings::values.pp_shader_name);
|
||||||
|
WriteGlobalSetting(Settings::values.anaglyph_shader_name);
|
||||||
WriteGlobalSetting(Settings::values.layout_option);
|
WriteGlobalSetting(Settings::values.layout_option);
|
||||||
WriteGlobalSetting(Settings::values.swap_screen);
|
WriteGlobalSetting(Settings::values.swap_screen);
|
||||||
WriteGlobalSetting(Settings::values.upright_screen);
|
WriteGlobalSetting(Settings::values.upright_screen);
|
||||||
@@ -1288,15 +1303,6 @@ QVariant Config::ReadSetting(const QString& name, const QVariant& default_value)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Type>
|
|
||||||
void Config::ReadSettingGlobal(Type& setting, const QString& name,
|
|
||||||
const QVariant& default_value) const {
|
|
||||||
const bool use_global = qt_config->value(name + QStringLiteral("/use_global"), true).toBool();
|
|
||||||
if (global || !use_global) {
|
|
||||||
setting = ReadSetting(name, default_value).value<Type>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Config::WriteSetting(const QString& name, const QVariant& value) {
|
void Config::WriteSetting(const QString& name, const QVariant& value) {
|
||||||
qt_config->setValue(name, value);
|
qt_config->setValue(name, value);
|
||||||
}
|
}
|
||||||
@@ -1307,17 +1313,6 @@ void Config::WriteSetting(const QString& name, const QVariant& value,
|
|||||||
qt_config->setValue(name, value);
|
qt_config->setValue(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::WriteSetting(const QString& name, const QVariant& value, const QVariant& default_value,
|
|
||||||
bool use_global) {
|
|
||||||
if (!global) {
|
|
||||||
qt_config->setValue(name + QStringLiteral("/use_global"), use_global);
|
|
||||||
}
|
|
||||||
if (global || !use_global) {
|
|
||||||
qt_config->setValue(name + QStringLiteral("/default"), value == default_value);
|
|
||||||
qt_config->setValue(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Config::Reload() {
|
void Config::Reload() {
|
||||||
ReadValues();
|
ReadValues();
|
||||||
// To apply default value changes
|
// To apply default value changes
|
||||||
|
@@ -84,31 +84,15 @@ private:
|
|||||||
QVariant ReadSetting(const QString& name) const;
|
QVariant ReadSetting(const QString& name) const;
|
||||||
QVariant ReadSetting(const QString& name, const QVariant& default_value) const;
|
QVariant ReadSetting(const QString& name, const QVariant& default_value) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Only reads a setting from the qt_config if the current config is a global config, or if the
|
|
||||||
* current config is a custom config and the setting is overriding the global setting. Otherwise
|
|
||||||
* it does nothing.
|
|
||||||
*
|
|
||||||
* @param setting The variable to be modified
|
|
||||||
* @param name The setting's identifier
|
|
||||||
* @param default_value The value to use when the setting is not already present in the config
|
|
||||||
*/
|
|
||||||
template <typename Type>
|
|
||||||
void ReadSettingGlobal(Type& setting, const QString& name, const QVariant& default_value) const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a setting to the qt_config.
|
* Writes a setting to the qt_config.
|
||||||
*
|
*
|
||||||
* @param name The setting's idetentifier
|
* @param name The setting's idetentifier
|
||||||
* @param value Value of the setting
|
* @param value Value of the setting
|
||||||
* @param default_value Default of the setting if not present in qt_config
|
* @param default_value Default of the setting if not present in qt_config
|
||||||
* @param use_global Specifies if the custom or global config should be in use, for custom
|
|
||||||
* configs
|
|
||||||
*/
|
*/
|
||||||
void WriteSetting(const QString& name, const QVariant& value);
|
void WriteSetting(const QString& name, const QVariant& value);
|
||||||
void WriteSetting(const QString& name, const QVariant& value, const QVariant& default_value);
|
void WriteSetting(const QString& name, const QVariant& value, const QVariant& default_value);
|
||||||
void WriteSetting(const QString& name, const QVariant& value, const QVariant& default_value,
|
|
||||||
bool use_global);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a value from the qt_config and applies it to the setting, using its label and default
|
* Reads a value from the qt_config and applies it to the setting, using its label and default
|
||||||
|
@@ -33,6 +33,16 @@ void ConfigurationShared::SetPerGameSetting(QCheckBox* checkbox,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void ConfigurationShared::SetPerGameSetting(
|
||||||
|
QComboBox* combobox, const Settings::SwitchableSetting<std::string>* setting) {
|
||||||
|
const int index =
|
||||||
|
static_cast<int>(combobox->findText(QString::fromStdString(setting->GetValue())));
|
||||||
|
combobox->setCurrentIndex(setting->UsingGlobal()
|
||||||
|
? ConfigurationShared::USE_GLOBAL_INDEX
|
||||||
|
: index + ConfigurationShared::USE_GLOBAL_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
void ConfigurationShared::SetHighlight(QWidget* widget, bool highlighted) {
|
void ConfigurationShared::SetHighlight(QWidget* widget, bool highlighted) {
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
widget->setStyleSheet(QStringLiteral("QWidget#%1 { background-color:rgba(0,203,255,0.5) }")
|
widget->setStyleSheet(QStringLiteral("QWidget#%1 { background-color:rgba(0,203,255,0.5) }")
|
||||||
|
@@ -78,6 +78,11 @@ void SetPerGameSetting(QComboBox* combobox,
|
|||||||
ConfigurationShared::USE_GLOBAL_OFFSET);
|
ConfigurationShared::USE_GLOBAL_OFFSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Specialization for string settings
|
||||||
|
template <>
|
||||||
|
void SetPerGameSetting(QComboBox* combobox,
|
||||||
|
const Settings::SwitchableSetting<std::string>* setting);
|
||||||
|
|
||||||
/// Given a Qt widget sets the background color to indicate whether the setting
|
/// Given a Qt widget sets the background color to indicate whether the setting
|
||||||
/// is per-game overriden (highlighted) or global (non-highlighted)
|
/// is per-game overriden (highlighted) or global (non-highlighted)
|
||||||
void SetHighlight(QWidget* widget, bool highlighted);
|
void SetHighlight(QWidget* widget, bool highlighted);
|
||||||
|
@@ -3,9 +3,9 @@
|
|||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include <QColorDialog>
|
#include <QColorDialog>
|
||||||
|
#include "citra_qt/configuration/configuration_shared.h"
|
||||||
#include "citra_qt/configuration/configure_enhancements.h"
|
#include "citra_qt/configuration/configure_enhancements.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "core/core.h"
|
|
||||||
#include "ui_configure_enhancements.h"
|
#include "ui_configure_enhancements.h"
|
||||||
#include "video_core/renderer_opengl/post_processing_opengl.h"
|
#include "video_core/renderer_opengl/post_processing_opengl.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
@@ -17,9 +17,10 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent)
|
|||||||
for (const auto& filter : OpenGL::TextureFilterer::GetFilterNames())
|
for (const auto& filter : OpenGL::TextureFilterer::GetFilterNames())
|
||||||
ui->texture_filter_combobox->addItem(QString::fromStdString(filter.data()));
|
ui->texture_filter_combobox->addItem(QString::fromStdString(filter.data()));
|
||||||
|
|
||||||
|
SetupPerGameUI();
|
||||||
SetConfiguration();
|
SetConfiguration();
|
||||||
|
|
||||||
ui->layoutBox->setEnabled(!Settings::values.custom_layout);
|
ui->layout_group->setEnabled(!Settings::values.custom_layout);
|
||||||
|
|
||||||
ui->resolution_factor_combobox->setEnabled(Settings::values.use_hw_renderer.GetValue());
|
ui->resolution_factor_combobox->setEnabled(Settings::values.use_hw_renderer.GetValue());
|
||||||
|
|
||||||
@@ -49,8 +50,33 @@ ConfigureEnhancements::ConfigureEnhancements(QWidget* parent)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigureEnhancements::~ConfigureEnhancements() = default;
|
||||||
|
|
||||||
void ConfigureEnhancements::SetConfiguration() {
|
void ConfigureEnhancements::SetConfiguration() {
|
||||||
ui->resolution_factor_combobox->setCurrentIndex(Settings::values.resolution_factor.GetValue());
|
|
||||||
|
if (!Settings::IsConfiguringGlobal()) {
|
||||||
|
ConfigurationShared::SetPerGameSetting(ui->resolution_factor_combobox,
|
||||||
|
&Settings::values.resolution_factor);
|
||||||
|
ConfigurationShared::SetPerGameSetting(ui->texture_filter_combobox,
|
||||||
|
&Settings::values.texture_filter_name);
|
||||||
|
ConfigurationShared::SetHighlight(ui->widget_texture_filter,
|
||||||
|
!Settings::values.texture_filter_name.UsingGlobal());
|
||||||
|
ConfigurationShared::SetPerGameSetting(ui->layout_combobox,
|
||||||
|
&Settings::values.layout_option);
|
||||||
|
} else {
|
||||||
|
ui->resolution_factor_combobox->setCurrentIndex(
|
||||||
|
Settings::values.resolution_factor.GetValue());
|
||||||
|
ui->layout_combobox->setCurrentIndex(
|
||||||
|
static_cast<int>(Settings::values.layout_option.GetValue()));
|
||||||
|
int tex_filter_idx = ui->texture_filter_combobox->findText(
|
||||||
|
QString::fromStdString(Settings::values.texture_filter_name.GetValue()));
|
||||||
|
if (tex_filter_idx == -1) {
|
||||||
|
ui->texture_filter_combobox->setCurrentIndex(0);
|
||||||
|
} else {
|
||||||
|
ui->texture_filter_combobox->setCurrentIndex(tex_filter_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ui->render_3d_combobox->setCurrentIndex(
|
ui->render_3d_combobox->setCurrentIndex(
|
||||||
static_cast<int>(Settings::values.render_3d.GetValue()));
|
static_cast<int>(Settings::values.render_3d.GetValue()));
|
||||||
ui->factor_3d->setValue(Settings::values.factor_3d.GetValue());
|
ui->factor_3d->setValue(Settings::values.factor_3d.GetValue());
|
||||||
@@ -58,17 +84,8 @@ void ConfigureEnhancements::SetConfiguration() {
|
|||||||
static_cast<int>(Settings::values.mono_render_option.GetValue()));
|
static_cast<int>(Settings::values.mono_render_option.GetValue()));
|
||||||
updateShaders(Settings::values.render_3d.GetValue());
|
updateShaders(Settings::values.render_3d.GetValue());
|
||||||
ui->toggle_linear_filter->setChecked(Settings::values.filter_mode.GetValue());
|
ui->toggle_linear_filter->setChecked(Settings::values.filter_mode.GetValue());
|
||||||
int tex_filter_idx = ui->texture_filter_combobox->findText(
|
ui->toggle_swap_screen->setChecked(Settings::values.swap_screen.GetValue());
|
||||||
QString::fromStdString(Settings::values.texture_filter_name.GetValue()));
|
ui->toggle_upright_screen->setChecked(Settings::values.upright_screen.GetValue());
|
||||||
if (tex_filter_idx == -1) {
|
|
||||||
ui->texture_filter_combobox->setCurrentIndex(0);
|
|
||||||
} else {
|
|
||||||
ui->texture_filter_combobox->setCurrentIndex(tex_filter_idx);
|
|
||||||
}
|
|
||||||
ui->layout_combobox->setCurrentIndex(
|
|
||||||
static_cast<int>(Settings::values.layout_option.GetValue()));
|
|
||||||
ui->swap_screen->setChecked(Settings::values.swap_screen.GetValue());
|
|
||||||
ui->upright_screen->setChecked(Settings::values.upright_screen.GetValue());
|
|
||||||
ui->large_screen_proportion->setValue(Settings::values.large_screen_proportion.GetValue());
|
ui->large_screen_proportion->setValue(Settings::values.large_screen_proportion.GetValue());
|
||||||
ui->toggle_dump_textures->setChecked(Settings::values.dump_textures.GetValue());
|
ui->toggle_dump_textures->setChecked(Settings::values.dump_textures.GetValue());
|
||||||
ui->toggle_custom_textures->setChecked(Settings::values.custom_textures.GetValue());
|
ui->toggle_custom_textures->setChecked(Settings::values.custom_textures.GetValue());
|
||||||
@@ -84,21 +101,31 @@ void ConfigureEnhancements::SetConfiguration() {
|
|||||||
|
|
||||||
void ConfigureEnhancements::updateShaders(Settings::StereoRenderOption stereo_option) {
|
void ConfigureEnhancements::updateShaders(Settings::StereoRenderOption stereo_option) {
|
||||||
ui->shader_combobox->clear();
|
ui->shader_combobox->clear();
|
||||||
|
ui->shader_combobox->setEnabled(true);
|
||||||
|
|
||||||
if (stereo_option == Settings::StereoRenderOption::Anaglyph)
|
if (stereo_option == Settings::StereoRenderOption::Interlaced ||
|
||||||
ui->shader_combobox->addItem(QStringLiteral("dubois (builtin)"));
|
stereo_option == Settings::StereoRenderOption::ReverseInterlaced) {
|
||||||
else if (stereo_option == Settings::StereoRenderOption::Interlaced ||
|
|
||||||
stereo_option == Settings::StereoRenderOption::ReverseInterlaced)
|
|
||||||
ui->shader_combobox->addItem(QStringLiteral("horizontal (builtin)"));
|
ui->shader_combobox->addItem(QStringLiteral("horizontal (builtin)"));
|
||||||
else
|
ui->shader_combobox->setCurrentIndex(0);
|
||||||
|
ui->shader_combobox->setEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string current_shader;
|
||||||
|
if (stereo_option == Settings::StereoRenderOption::Anaglyph) {
|
||||||
|
ui->shader_combobox->addItem(QStringLiteral("dubois (builtin)"));
|
||||||
|
current_shader = Settings::values.anaglyph_shader_name.GetValue();
|
||||||
|
} else {
|
||||||
ui->shader_combobox->addItem(QStringLiteral("none (builtin)"));
|
ui->shader_combobox->addItem(QStringLiteral("none (builtin)"));
|
||||||
|
current_shader = Settings::values.pp_shader_name.GetValue();
|
||||||
|
}
|
||||||
|
|
||||||
ui->shader_combobox->setCurrentIndex(0);
|
ui->shader_combobox->setCurrentIndex(0);
|
||||||
|
|
||||||
for (const auto& shader : OpenGL::GetPostProcessingShaderList(
|
for (const auto& shader : OpenGL::GetPostProcessingShaderList(
|
||||||
stereo_option == Settings::StereoRenderOption::Anaglyph)) {
|
stereo_option == Settings::StereoRenderOption::Anaglyph)) {
|
||||||
ui->shader_combobox->addItem(QString::fromStdString(shader));
|
ui->shader_combobox->addItem(QString::fromStdString(shader));
|
||||||
if (Settings::values.pp_shader_name.GetValue() == shader)
|
if (current_shader == shader)
|
||||||
ui->shader_combobox->setCurrentIndex(ui->shader_combobox->count() - 1);
|
ui->shader_combobox->setCurrentIndex(ui->shader_combobox->count() - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,28 +135,83 @@ void ConfigureEnhancements::RetranslateUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureEnhancements::ApplyConfiguration() {
|
void ConfigureEnhancements::ApplyConfiguration() {
|
||||||
Settings::values.resolution_factor =
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.resolution_factor,
|
||||||
static_cast<u16>(ui->resolution_factor_combobox->currentIndex());
|
ui->resolution_factor_combobox);
|
||||||
Settings::values.render_3d =
|
Settings::values.render_3d =
|
||||||
static_cast<Settings::StereoRenderOption>(ui->render_3d_combobox->currentIndex());
|
static_cast<Settings::StereoRenderOption>(ui->render_3d_combobox->currentIndex());
|
||||||
Settings::values.factor_3d = ui->factor_3d->value();
|
Settings::values.factor_3d = ui->factor_3d->value();
|
||||||
Settings::values.mono_render_option =
|
Settings::values.mono_render_option =
|
||||||
static_cast<Settings::MonoRenderOption>(ui->mono_rendering_eye->currentIndex());
|
static_cast<Settings::MonoRenderOption>(ui->mono_rendering_eye->currentIndex());
|
||||||
Settings::values.pp_shader_name =
|
if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Anaglyph) {
|
||||||
ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString();
|
Settings::values.anaglyph_shader_name =
|
||||||
Settings::values.filter_mode = ui->toggle_linear_filter->isChecked();
|
ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString();
|
||||||
Settings::values.texture_filter_name = ui->texture_filter_combobox->currentText().toStdString();
|
} else if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Off) {
|
||||||
Settings::values.layout_option =
|
Settings::values.pp_shader_name =
|
||||||
static_cast<Settings::LayoutOption>(ui->layout_combobox->currentIndex());
|
ui->shader_combobox->itemText(ui->shader_combobox->currentIndex()).toStdString();
|
||||||
Settings::values.swap_screen = ui->swap_screen->isChecked();
|
}
|
||||||
Settings::values.upright_screen = ui->upright_screen->isChecked();
|
|
||||||
Settings::values.large_screen_proportion = ui->large_screen_proportion->value();
|
Settings::values.large_screen_proportion = ui->large_screen_proportion->value();
|
||||||
Settings::values.dump_textures = ui->toggle_dump_textures->isChecked();
|
|
||||||
Settings::values.custom_textures = ui->toggle_custom_textures->isChecked();
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.filter_mode,
|
||||||
Settings::values.preload_textures = ui->toggle_preload_textures->isChecked();
|
ui->toggle_linear_filter, linear_filter);
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(
|
||||||
|
&Settings::values.texture_filter_name, ui->texture_filter_combobox,
|
||||||
|
[this](int index) { return ui->texture_filter_combobox->itemText(index).toStdString(); });
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.layout_option, ui->layout_combobox);
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.swap_screen, ui->toggle_swap_screen,
|
||||||
|
swap_screen);
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.upright_screen,
|
||||||
|
ui->toggle_upright_screen, upright_screen);
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.dump_textures,
|
||||||
|
ui->toggle_dump_textures, dump_textures);
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.custom_textures,
|
||||||
|
ui->toggle_custom_textures, custom_textures);
|
||||||
|
ConfigurationShared::ApplyPerGameSetting(&Settings::values.preload_textures,
|
||||||
|
ui->toggle_preload_textures, preload_textures);
|
||||||
|
|
||||||
Settings::values.bg_red = static_cast<float>(bg_color.redF());
|
Settings::values.bg_red = static_cast<float>(bg_color.redF());
|
||||||
Settings::values.bg_green = static_cast<float>(bg_color.greenF());
|
Settings::values.bg_green = static_cast<float>(bg_color.greenF());
|
||||||
Settings::values.bg_blue = static_cast<float>(bg_color.blueF());
|
Settings::values.bg_blue = static_cast<float>(bg_color.blueF());
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigureEnhancements::~ConfigureEnhancements() {}
|
void ConfigureEnhancements::SetupPerGameUI() {
|
||||||
|
// Block the global settings if a game is currently running that overrides them
|
||||||
|
if (Settings::IsConfiguringGlobal()) {
|
||||||
|
ui->widget_resolution->setEnabled(Settings::values.resolution_factor.UsingGlobal());
|
||||||
|
ui->widget_texture_filter->setEnabled(Settings::values.texture_filter_name.UsingGlobal());
|
||||||
|
ui->toggle_linear_filter->setEnabled(Settings::values.filter_mode.UsingGlobal());
|
||||||
|
ui->toggle_swap_screen->setEnabled(Settings::values.swap_screen.UsingGlobal());
|
||||||
|
ui->toggle_upright_screen->setEnabled(Settings::values.upright_screen.UsingGlobal());
|
||||||
|
ui->toggle_dump_textures->setEnabled(Settings::values.dump_textures.UsingGlobal());
|
||||||
|
ui->toggle_custom_textures->setEnabled(Settings::values.custom_textures.UsingGlobal());
|
||||||
|
ui->toggle_preload_textures->setEnabled(Settings::values.preload_textures.UsingGlobal());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui->stereo_group->setVisible(false);
|
||||||
|
ui->widget_shader->setVisible(false);
|
||||||
|
ui->bg_color_group->setVisible(false);
|
||||||
|
|
||||||
|
ConfigurationShared::SetColoredTristate(ui->toggle_linear_filter, Settings::values.filter_mode,
|
||||||
|
linear_filter);
|
||||||
|
ConfigurationShared::SetColoredTristate(ui->toggle_swap_screen, Settings::values.swap_screen,
|
||||||
|
swap_screen);
|
||||||
|
ConfigurationShared::SetColoredTristate(ui->toggle_upright_screen,
|
||||||
|
Settings::values.upright_screen, upright_screen);
|
||||||
|
ConfigurationShared::SetColoredTristate(ui->toggle_dump_textures,
|
||||||
|
Settings::values.dump_textures, dump_textures);
|
||||||
|
ConfigurationShared::SetColoredTristate(ui->toggle_custom_textures,
|
||||||
|
Settings::values.custom_textures, custom_textures);
|
||||||
|
ConfigurationShared::SetColoredTristate(ui->toggle_preload_textures,
|
||||||
|
Settings::values.preload_textures, preload_textures);
|
||||||
|
|
||||||
|
ConfigurationShared::SetColoredComboBox(
|
||||||
|
ui->resolution_factor_combobox, ui->widget_resolution,
|
||||||
|
static_cast<u32>(Settings::values.resolution_factor.GetValue(true)));
|
||||||
|
|
||||||
|
ConfigurationShared::SetColoredComboBox(ui->texture_filter_combobox, ui->widget_texture_filter,
|
||||||
|
0);
|
||||||
|
|
||||||
|
ConfigurationShared::SetColoredComboBox(
|
||||||
|
ui->layout_combobox, ui->widget_layout,
|
||||||
|
static_cast<u32>(Settings::values.layout_option.GetValue(true)));
|
||||||
|
}
|
||||||
|
@@ -12,6 +12,10 @@ namespace Settings {
|
|||||||
enum class StereoRenderOption : u32;
|
enum class StereoRenderOption : u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace ConfigurationShared {
|
||||||
|
enum class CheckState;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class ConfigureEnhancements;
|
class ConfigureEnhancements;
|
||||||
}
|
}
|
||||||
@@ -27,10 +31,18 @@ public:
|
|||||||
void RetranslateUI();
|
void RetranslateUI();
|
||||||
void SetConfiguration();
|
void SetConfiguration();
|
||||||
|
|
||||||
|
void SetupPerGameUI();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateShaders(Settings::StereoRenderOption stereo_option);
|
void updateShaders(Settings::StereoRenderOption stereo_option);
|
||||||
void updateTextureFilter(int index);
|
void updateTextureFilter(int index);
|
||||||
|
|
||||||
std::unique_ptr<Ui::ConfigureEnhancements> ui;
|
std::unique_ptr<Ui::ConfigureEnhancements> ui;
|
||||||
|
ConfigurationShared::CheckState linear_filter;
|
||||||
|
ConfigurationShared::CheckState swap_screen;
|
||||||
|
ConfigurationShared::CheckState upright_screen;
|
||||||
|
ConfigurationShared::CheckState dump_textures;
|
||||||
|
ConfigurationShared::CheckState custom_textures;
|
||||||
|
ConfigurationShared::CheckState preload_textures;
|
||||||
QColor bg_color;
|
QColor bg_color;
|
||||||
};
|
};
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>400</width>
|
<width>400</width>
|
||||||
<height>634</height>
|
<height>657</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
@@ -27,74 +27,88 @@
|
|||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<widget class="QWidget" name="widget_resolution" native="true">
|
||||||
<item>
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<widget class="QLabel" name="label">
|
<property name="leftMargin">
|
||||||
<property name="text">
|
<number>0</number>
|
||||||
<string>Internal Resolution</string>
|
</property>
|
||||||
</property>
|
<property name="topMargin">
|
||||||
</widget>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
<item>
|
<property name="rightMargin">
|
||||||
<widget class="QComboBox" name="resolution_factor_combobox">
|
<number>0</number>
|
||||||
<item>
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="resolution_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Auto (Window Size)</string>
|
<string>Internal Resolution</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
</widget>
|
||||||
<item>
|
</item>
|
||||||
<property name="text">
|
<item>
|
||||||
<string>Native (400x240)</string>
|
<widget class="QComboBox" name="resolution_factor_combobox">
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>Auto (Window Size)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>2x Native (800x480)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>Native (400x240)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>3x Native (1200x720)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>2x Native (800x480)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>4x Native (1600x960)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>3x Native (1200x720)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>5x Native (2000x1200)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>4x Native (1600x960)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>6x Native (2400x1440)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>5x Native (2000x1200)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>7x Native (2800x1680)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>6x Native (2400x1440)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>8x Native (3200x1920)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>7x Native (2800x1680)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>9x Native (3600x2160)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>8x Native (3200x1920)</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>10x Native (4000x2400)</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
</widget>
|
<string>9x Native (3600x2160)</string>
|
||||||
</item>
|
</property>
|
||||||
</layout>
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>10x Native (4000x2400)</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="toggle_linear_filter">
|
<widget class="QCheckBox" name="toggle_linear_filter">
|
||||||
@@ -104,38 +118,66 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
<widget class="QWidget" name="widget_shader" native="true">
|
||||||
<item>
|
<layout class="QHBoxLayout" name="horizontalLayout_11">
|
||||||
<widget class="QLabel" name="label_2">
|
<property name="leftMargin">
|
||||||
<property name="text">
|
<number>0</number>
|
||||||
<string>Post-Processing Shader</string>
|
</property>
|
||||||
</property>
|
<property name="topMargin">
|
||||||
</widget>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
<item>
|
<property name="rightMargin">
|
||||||
<widget class="QComboBox" name="shader_combobox"/>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
</layout>
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="shader_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Post-Processing Shader</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="shader_combobox"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<widget class="QWidget" name="widget_texture_filter" native="true">
|
||||||
<item>
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
<widget class="QLabel" name="label_5">
|
<property name="leftMargin">
|
||||||
<property name="text">
|
<number>0</number>
|
||||||
<string>Texture Filter</string>
|
</property>
|
||||||
</property>
|
<property name="topMargin">
|
||||||
</widget>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
<item>
|
<property name="rightMargin">
|
||||||
<widget class="QComboBox" name="texture_filter_combobox"/>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
</layout>
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="texture_filter_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Texture Filter</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="texture_filter_combobox"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="stereo_group">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Stereoscopy</string>
|
<string>Stereoscopy</string>
|
||||||
</property>
|
</property>
|
||||||
@@ -236,109 +278,151 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="layoutBox">
|
<widget class="QGroupBox" name="layout_group">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Layout</string>
|
<string>Layout</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
<widget class="QWidget" name="widget_layout" native="true">
|
||||||
<item>
|
<layout class="QHBoxLayout" name="screen_layout_group">
|
||||||
<widget class="QLabel" name="label1">
|
<property name="leftMargin">
|
||||||
<property name="text">
|
<number>0</number>
|
||||||
<string>Screen Layout:</string>
|
</property>
|
||||||
</property>
|
<property name="topMargin">
|
||||||
</widget>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
<item>
|
<property name="rightMargin">
|
||||||
<widget class="QComboBox" name="layout_combobox">
|
<number>0</number>
|
||||||
<item>
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="layout_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Default</string>
|
<string>Screen Layout:</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
</widget>
|
||||||
<item>
|
</item>
|
||||||
<property name="text">
|
<item>
|
||||||
<string>Single Screen</string>
|
<widget class="QComboBox" name="layout_combobox">
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>Default</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>Large Screen</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>Single Screen</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>Side by Side</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
<item>
|
<string>Large Screen</string>
|
||||||
<property name="text">
|
</property>
|
||||||
<string>Separate Windows</string>
|
</item>
|
||||||
</property>
|
<item>
|
||||||
</item>
|
<property name="text">
|
||||||
</widget>
|
<string>Side by Side</string>
|
||||||
</item>
|
</property>
|
||||||
</layout>
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Separate Windows</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="swap_screen">
|
<widget class="QCheckBox" name="toggle_swap_screen">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Swap Screens</string>
|
<string>Swap Screens</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="upright_screen">
|
<widget class="QCheckBox" name="toggle_upright_screen">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Rotate Screens Upright</string>
|
<string>Rotate Screens Upright</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
<widget class="QWidget" name="" native="true">
|
||||||
<item>
|
<layout class="QHBoxLayout" name="proportion_layout">
|
||||||
<widget class="QLabel" name="label_3">
|
<property name="leftMargin">
|
||||||
<property name="text">
|
<number>0</number>
|
||||||
<string>Large Screen Proportion:</string>
|
</property>
|
||||||
</property>
|
<property name="topMargin">
|
||||||
</widget>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
<item>
|
<property name="rightMargin">
|
||||||
<widget class="QDoubleSpinBox" name="large_screen_proportion">
|
<number>0</number>
|
||||||
<property name="minimum">
|
</property>
|
||||||
<number>1</number>
|
<property name="bottomMargin">
|
||||||
</property>
|
<number>0</number>
|
||||||
<property name="maximum">
|
</property>
|
||||||
<number>16</number>
|
<item>
|
||||||
</property>
|
<widget class="QLabel" name="label_3">
|
||||||
<property name="value">
|
<property name="text">
|
||||||
<number>4</number>
|
<string>Large Screen Proportion:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDoubleSpinBox" name="large_screen_proportion">
|
||||||
|
<property name="minimum">
|
||||||
|
<double>1.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>16.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<double>4.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
<widget class="QWidget" name="bg_color_group" native="true">
|
||||||
<item>
|
<layout class="QHBoxLayout" name="bg_color_group_2">
|
||||||
<widget class="QLabel" name="bg_label">
|
<property name="leftMargin">
|
||||||
<property name="text">
|
<number>0</number>
|
||||||
<string>Background Color:</string>
|
</property>
|
||||||
</property>
|
<property name="topMargin">
|
||||||
</widget>
|
<number>0</number>
|
||||||
</item>
|
</property>
|
||||||
<item>
|
<property name="rightMargin">
|
||||||
<widget class="QPushButton" name="bg_button">
|
<number>0</number>
|
||||||
<property name="maximumSize">
|
</property>
|
||||||
<size>
|
<property name="bottomMargin">
|
||||||
<width>40</width>
|
<number>0</number>
|
||||||
<height>16777215</height>
|
</property>
|
||||||
</size>
|
<item>
|
||||||
</property>
|
<widget class="QLabel" name="bg_label">
|
||||||
</widget>
|
<property name="text">
|
||||||
</item>
|
<string>Background Color:</string>
|
||||||
</layout>
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="bg_button">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
@@ -406,8 +490,8 @@
|
|||||||
<tabstop>factor_3d</tabstop>
|
<tabstop>factor_3d</tabstop>
|
||||||
<tabstop>mono_rendering_eye</tabstop>
|
<tabstop>mono_rendering_eye</tabstop>
|
||||||
<tabstop>layout_combobox</tabstop>
|
<tabstop>layout_combobox</tabstop>
|
||||||
<tabstop>swap_screen</tabstop>
|
<tabstop>toggle_swap_screen</tabstop>
|
||||||
<tabstop>upright_screen</tabstop>
|
<tabstop>toggle_upright_screen</tabstop>
|
||||||
<tabstop>large_screen_proportion</tabstop>
|
<tabstop>large_screen_proportion</tabstop>
|
||||||
<tabstop>bg_button</tabstop>
|
<tabstop>bg_button</tabstop>
|
||||||
<tabstop>toggle_custom_textures</tabstop>
|
<tabstop>toggle_custom_textures</tabstop>
|
||||||
|
@@ -10,6 +10,7 @@
|
|||||||
#include "citra_qt/configuration/config.h"
|
#include "citra_qt/configuration/config.h"
|
||||||
#include "citra_qt/configuration/configure_audio.h"
|
#include "citra_qt/configuration/configure_audio.h"
|
||||||
#include "citra_qt/configuration/configure_debug.h"
|
#include "citra_qt/configuration/configure_debug.h"
|
||||||
|
#include "citra_qt/configuration/configure_enhancements.h"
|
||||||
#include "citra_qt/configuration/configure_general.h"
|
#include "citra_qt/configuration/configure_general.h"
|
||||||
#include "citra_qt/configuration/configure_graphics.h"
|
#include "citra_qt/configuration/configure_graphics.h"
|
||||||
#include "citra_qt/configuration/configure_per_game.h"
|
#include "citra_qt/configuration/configure_per_game.h"
|
||||||
@@ -30,6 +31,7 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const QString
|
|||||||
|
|
||||||
audio_tab = std::make_unique<ConfigureAudio>(this);
|
audio_tab = std::make_unique<ConfigureAudio>(this);
|
||||||
general_tab = std::make_unique<ConfigureGeneral>(this);
|
general_tab = std::make_unique<ConfigureGeneral>(this);
|
||||||
|
enhancements_tab = std::make_unique<ConfigureEnhancements>(this);
|
||||||
graphics_tab = std::make_unique<ConfigureGraphics>(this);
|
graphics_tab = std::make_unique<ConfigureGraphics>(this);
|
||||||
system_tab = std::make_unique<ConfigureSystem>(this);
|
system_tab = std::make_unique<ConfigureSystem>(this);
|
||||||
debug_tab = std::make_unique<ConfigureDebug>(this);
|
debug_tab = std::make_unique<ConfigureDebug>(this);
|
||||||
@@ -38,6 +40,7 @@ ConfigurePerGame::ConfigurePerGame(QWidget* parent, u64 title_id_, const QString
|
|||||||
|
|
||||||
ui->tabWidget->addTab(general_tab.get(), tr("General"));
|
ui->tabWidget->addTab(general_tab.get(), tr("General"));
|
||||||
ui->tabWidget->addTab(system_tab.get(), tr("System"));
|
ui->tabWidget->addTab(system_tab.get(), tr("System"));
|
||||||
|
ui->tabWidget->addTab(enhancements_tab.get(), tr("Enhancements"));
|
||||||
ui->tabWidget->addTab(graphics_tab.get(), tr("Graphics"));
|
ui->tabWidget->addTab(graphics_tab.get(), tr("Graphics"));
|
||||||
ui->tabWidget->addTab(audio_tab.get(), tr("Audio"));
|
ui->tabWidget->addTab(audio_tab.get(), tr("Audio"));
|
||||||
ui->tabWidget->addTab(debug_tab.get(), tr("Debug"));
|
ui->tabWidget->addTab(debug_tab.get(), tr("Debug"));
|
||||||
@@ -81,10 +84,12 @@ void ConfigurePerGame::ResetDefaults() {
|
|||||||
void ConfigurePerGame::ApplyConfiguration() {
|
void ConfigurePerGame::ApplyConfiguration() {
|
||||||
general_tab->ApplyConfiguration();
|
general_tab->ApplyConfiguration();
|
||||||
system_tab->ApplyConfiguration();
|
system_tab->ApplyConfiguration();
|
||||||
|
enhancements_tab->ApplyConfiguration();
|
||||||
graphics_tab->ApplyConfiguration();
|
graphics_tab->ApplyConfiguration();
|
||||||
audio_tab->ApplyConfiguration();
|
audio_tab->ApplyConfiguration();
|
||||||
debug_tab->ApplyConfiguration();
|
debug_tab->ApplyConfiguration();
|
||||||
|
|
||||||
|
Settings::Apply();
|
||||||
Settings::LogSettings();
|
Settings::LogSettings();
|
||||||
|
|
||||||
game_config->Save();
|
game_config->Save();
|
||||||
|
@@ -15,6 +15,7 @@ class System;
|
|||||||
|
|
||||||
class ConfigureAudio;
|
class ConfigureAudio;
|
||||||
class ConfigureGeneral;
|
class ConfigureGeneral;
|
||||||
|
class ConfigureEnhancements;
|
||||||
class ConfigureGraphics;
|
class ConfigureGraphics;
|
||||||
class ConfigureSystem;
|
class ConfigureSystem;
|
||||||
class ConfigureDebug;
|
class ConfigureDebug;
|
||||||
@@ -65,6 +66,7 @@ private:
|
|||||||
|
|
||||||
std::unique_ptr<ConfigureAudio> audio_tab;
|
std::unique_ptr<ConfigureAudio> audio_tab;
|
||||||
std::unique_ptr<ConfigureGeneral> general_tab;
|
std::unique_ptr<ConfigureGeneral> general_tab;
|
||||||
|
std::unique_ptr<ConfigureEnhancements> enhancements_tab;
|
||||||
std::unique_ptr<ConfigureGraphics> graphics_tab;
|
std::unique_ptr<ConfigureGraphics> graphics_tab;
|
||||||
std::unique_ptr<ConfigureSystem> system_tab;
|
std::unique_ptr<ConfigureSystem> system_tab;
|
||||||
std::unique_ptr<ConfigureDebug> debug_tab;
|
std::unique_ptr<ConfigureDebug> debug_tab;
|
||||||
|
@@ -263,6 +263,8 @@ ConfigureSystem::ConfigureSystem(QWidget* parent)
|
|||||||
} else {
|
} else {
|
||||||
ui->button_start_download->setEnabled(false);
|
ui->button_start_download->setEnabled(false);
|
||||||
ui->combo_download_mode->setEnabled(false);
|
ui->combo_download_mode->setEnabled(false);
|
||||||
|
ui->label_nus_download->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||||
|
ui->label_nus_download->setOpenExternalLinks(true);
|
||||||
ui->label_nus_download->setText(
|
ui->label_nus_download->setText(
|
||||||
tr("Citra is missing keys to download system files. <br><a "
|
tr("Citra is missing keys to download system files. <br><a "
|
||||||
"href='https://citra-emu.org/wiki/aes-keys/'><span style=\"text-decoration: "
|
"href='https://citra-emu.org/wiki/aes-keys/'><span style=\"text-decoration: "
|
||||||
|
@@ -92,6 +92,11 @@
|
|||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef __unix__
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QtDBus/QDBusInterface>
|
||||||
|
#include <QtDBus/QtDBus>
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DISCORD_PRESENCE
|
#ifdef USE_DISCORD_PRESENCE
|
||||||
#include "citra_qt/discord_impl.h"
|
#include "citra_qt/discord_impl.h"
|
||||||
@@ -958,16 +963,72 @@ void GMainWindow::OnOpenUpdater() {
|
|||||||
updater->LaunchUI();
|
updater->LaunchUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__unix__) && !defined(__APPLE__)
|
||||||
|
static std::optional<QDBusObjectPath> HoldWakeLockLinux(u32 window_id = 0) {
|
||||||
|
if (!QDBusConnection::sessionBus().isConnected()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit
|
||||||
|
QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"),
|
||||||
|
QStringLiteral("/org/freedesktop/portal/desktop"),
|
||||||
|
QStringLiteral("org.freedesktop.portal.Inhibit"));
|
||||||
|
if (!xdp.isValid()) {
|
||||||
|
LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
QVariantMap options = {};
|
||||||
|
//: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the
|
||||||
|
//: computer from sleeping
|
||||||
|
options.insert(QString::fromLatin1("reason"),
|
||||||
|
QCoreApplication::translate("GMainWindow", "Citra is running a game"));
|
||||||
|
// 0x4: Suspend lock; 0x8: Idle lock
|
||||||
|
QDBusReply<QDBusObjectPath> reply =
|
||||||
|
xdp.call(QString::fromLatin1("Inhibit"),
|
||||||
|
QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options);
|
||||||
|
|
||||||
|
if (reply.isValid()) {
|
||||||
|
return reply.value();
|
||||||
|
}
|
||||||
|
LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}",
|
||||||
|
reply.error().message().toStdString());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) {
|
||||||
|
if (!QDBusConnection::sessionBus().isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(),
|
||||||
|
QString::fromLatin1("org.freedesktop.portal.Request"));
|
||||||
|
unlocker.call(QString::fromLatin1("Close"));
|
||||||
|
}
|
||||||
|
#endif // __unix__
|
||||||
|
|
||||||
void GMainWindow::PreventOSSleep() {
|
void GMainWindow::PreventOSSleep() {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);
|
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);
|
||||||
#endif
|
#elif defined(HAVE_SDL2)
|
||||||
|
SDL_DisableScreenSaver();
|
||||||
|
#ifdef __unix__
|
||||||
|
auto reply = HoldWakeLockLinux(winId());
|
||||||
|
if (reply) {
|
||||||
|
wake_lock = std::move(reply.value());
|
||||||
|
}
|
||||||
|
#endif // __unix__
|
||||||
|
#endif // _WIN32
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::AllowOSSleep() {
|
void GMainWindow::AllowOSSleep() {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
SetThreadExecutionState(ES_CONTINUOUS);
|
SetThreadExecutionState(ES_CONTINUOUS);
|
||||||
#endif
|
#elif defined(HAVE_SDL2)
|
||||||
|
SDL_EnableScreenSaver();
|
||||||
|
#ifdef __unix__
|
||||||
|
if (!wake_lock.path().isEmpty()) {
|
||||||
|
ReleaseWakeLockLinux(wake_lock);
|
||||||
|
}
|
||||||
|
#endif // __unix__
|
||||||
|
#endif // _WIN32
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GMainWindow::LoadROM(const QString& filename) {
|
bool GMainWindow::LoadROM(const QString& filename) {
|
||||||
|
@@ -17,6 +17,10 @@
|
|||||||
#include "core/hle/service/am/am.h"
|
#include "core/hle/service/am/am.h"
|
||||||
#include "core/savestate.h"
|
#include "core/savestate.h"
|
||||||
|
|
||||||
|
#ifdef __unix__
|
||||||
|
#include <QDBusObjectPath>
|
||||||
|
#endif
|
||||||
|
|
||||||
class AboutDialog;
|
class AboutDialog;
|
||||||
class Config;
|
class Config;
|
||||||
class ClickableLabel;
|
class ClickableLabel;
|
||||||
@@ -332,6 +336,10 @@ private:
|
|||||||
|
|
||||||
HotkeyRegistry hotkey_registry;
|
HotkeyRegistry hotkey_registry;
|
||||||
|
|
||||||
|
#ifdef __unix__
|
||||||
|
QDBusObjectPath wake_lock{};
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void dropEvent(QDropEvent* event) override;
|
void dropEvent(QDropEvent* event) override;
|
||||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||||
|