Add support for slot 2 Memory Expansion

This commit is contained in:
Rafael Caetano 2024-06-27 19:24:26 +01:00
parent ec8a89e341
commit 0a3ee936c0
72 changed files with 521 additions and 649 deletions

View File

@ -6,6 +6,7 @@
#include <unistd.h>
#include <cstdlib>
#include <MelonDS.h>
#include <RomGbaSlotConfig.h>
#include <InputAndroid.h>
#include <android/asset_manager_jni.h>
#include "UriFileHandler.h"
@ -18,7 +19,14 @@
#define MAX_CHEAT_SIZE (2*64)
enum GbaSlotType {
NONE = 0,
GBA_ROM = 1,
MEMORY_EXPANSION = 2,
};
void* emulate(void*);
MelonDSAndroid::RomGbaSlotConfig* buildGbaSlotConfig(GbaSlotType slotType, const char* romPath, const char* savePath);
pthread_t emuThread;
pthread_mutex_t emuThreadMutex;
@ -186,7 +194,7 @@ Java_me_magnum_melonds_MelonEmulator_getRichPresenceStatus(JNIEnv* env, jobject
}
JNIEXPORT jint JNICALL
Java_me_magnum_melonds_MelonEmulator_loadRomInternal(JNIEnv* env, jobject thiz, jstring romPath, jstring sramPath, jboolean loadGbaRom, jstring gbaRomPath, jstring gbaSramPath)
Java_me_magnum_melonds_MelonEmulator_loadRomInternal(JNIEnv* env, jobject thiz, jstring romPath, jstring sramPath, jint gbaSlotType, jstring gbaRomPath, jstring gbaSramPath)
{
jboolean isCopy = JNI_FALSE;
const char* rom = romPath == nullptr ? nullptr : env->GetStringUTFChars(romPath, &isCopy);
@ -194,7 +202,9 @@ Java_me_magnum_melonds_MelonEmulator_loadRomInternal(JNIEnv* env, jobject thiz,
const char* gbaRom = gbaRomPath == nullptr ? nullptr : env->GetStringUTFChars(gbaRomPath, &isCopy);
const char* gbaSram = gbaSramPath == nullptr ? nullptr : env->GetStringUTFChars(gbaSramPath, &isCopy);
int result = MelonDSAndroid::loadRom(const_cast<char*>(rom), const_cast<char*>(sram), loadGbaRom, const_cast<char*>(gbaRom), const_cast<char*>(gbaSram));
MelonDSAndroid::RomGbaSlotConfig* gbaSlotConfig = buildGbaSlotConfig((GbaSlotType) gbaSlotType, gbaRom, gbaSram);
int result = MelonDSAndroid::loadRom(const_cast<char*>(rom), const_cast<char*>(sram), gbaSlotConfig);
delete gbaSlotConfig;
if (isCopy == JNI_TRUE) {
if (romPath) env->ReleaseStringUTFChars(romPath, rom);
@ -467,6 +477,26 @@ Java_me_magnum_melonds_MelonEmulator_updateEmulatorConfiguration(JNIEnv* env, jo
}
}
MelonDSAndroid::RomGbaSlotConfig* buildGbaSlotConfig(GbaSlotType slotType, const char* romPath, const char* savePath)
{
if (slotType == GbaSlotType::GBA_ROM)
{
MelonDSAndroid::RomGbaSlotConfigGbaRom* gbaSlotConfigGbaRom = new MelonDSAndroid::RomGbaSlotConfigGbaRom {
.romPath = std::string(romPath),
.savePath = std::string(savePath)
};
return (MelonDSAndroid::RomGbaSlotConfig*) gbaSlotConfigGbaRom;
}
else if (slotType == GbaSlotType::MEMORY_EXPANSION)
{
return (MelonDSAndroid::RomGbaSlotConfig*) new MelonDSAndroid::RomGbaSlotConfigMemoryExpansion;
}
else
{
return (MelonDSAndroid::RomGbaSlotConfig*) new MelonDSAndroid::RomGbaSlotConfigNone;
}
}
double getCurrentMillis() {
timespec now;
clock_gettime(CLOCK_REALTIME, &now);

View File

@ -37,6 +37,12 @@ object MelonEmulator {
DSI_NAND_BAD
}
enum class GbaSlotType {
NONE,
GBA_ROM,
MEMORY_EXPANSION,
}
external fun setupEmulator(emulatorConfiguration: EmulatorConfiguration, assetManager: AssetManager?, dsiCameraSource: DSiCameraSource?, retroAchievementsCallback: RetroAchievementsCallback, textureBuffer: ByteBuffer)
external fun setupCheats(cheats: Array<Cheat>)
@ -47,8 +53,8 @@ object MelonEmulator {
external fun getRichPresenceStatus(): String?
fun loadRom(romUri: Uri, sramUri: Uri, loadGbaRom: Boolean, gbaRomUri: Uri?, gbaSramUri: Uri?): LoadResult {
val loadResult = loadRomInternal(romUri.toString(), sramUri.toString(), loadGbaRom, gbaRomUri?.toString(), gbaSramUri?.toString())
fun loadRom(romUri: Uri, sramUri: Uri, gbaSlotType: GbaSlotType, gbaRomUri: Uri?, gbaSramUri: Uri?): LoadResult {
val loadResult = loadRomInternal(romUri.toString(), sramUri.toString(), gbaSlotType.ordinal, gbaRomUri?.toString(), gbaSramUri?.toString())
return when (loadResult) {
0 -> LoadResult.SUCCESS
1 -> LoadResult.SUCCESS_GBA_FAILED
@ -63,7 +69,7 @@ object MelonEmulator {
return FirmwareLoadResult.entries[loadResult]
}
private external fun loadRomInternal(romPath: String, sramPath: String, loadGbaRom: Boolean, gbaRomPath: String?, gbaSramPath: String?): Int
private external fun loadRomInternal(romPath: String, sramPath: String, gbaSlotType: Int, gbaRomPath: String?, gbaSramPath: String?): Int
private external fun bootFirmwareInternal(): Int

View File

@ -6,6 +6,8 @@ import android.net.Uri
import io.reactivex.Single
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.*
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.extensions.isBlank
import me.magnum.melonds.extensions.nameWithoutExtension
import me.magnum.melonds.impl.NdsRomCache

View File

@ -5,8 +5,8 @@ import android.graphics.Bitmap
import android.net.Uri
import io.reactivex.Single
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.RomInfo
import me.magnum.melonds.domain.model.RomMetadata
import me.magnum.melonds.extensions.isBlank

View File

@ -3,7 +3,7 @@ package me.magnum.melonds.common.romprocessors
import android.graphics.Bitmap
import android.net.Uri
import io.reactivex.Single
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.RomInfo
interface RomFileProcessor {

View File

@ -47,6 +47,7 @@ object MigrationModule {
registerMigration(Migration21to22(context, gson, uriHandler))
registerMigration(Migration24to25(genericJsonArrayMigrationHelper, context))
registerMigration(Migration25to26(genericJsonArrayMigrationHelper))
registerMigration(Migration30to31(genericJsonArrayMigrationHelper))
}
}
}

View File

@ -1,15 +0,0 @@
package me.magnum.melonds.domain.model
import android.net.Uri
import java.util.*
data class RomConfig(
var runtimeConsoleType: RuntimeConsoleType = RuntimeConsoleType.DEFAULT,
var runtimeMicSource: RuntimeMicSource = RuntimeMicSource.DEFAULT,
var layoutId: UUID? = null,
var loadGbaCart: Boolean = false,
var gbaCartPath: Uri? = null,
var gbaSavePath: Uri? = null
) {
fun mustLoadGbaCart() = loadGbaCart && gbaCartPath != null
}

View File

@ -1,6 +1,7 @@
package me.magnum.melonds.domain.model
package me.magnum.melonds.domain.model.rom
import android.net.Uri
import me.magnum.melonds.domain.model.rom.config.RomConfig
import java.util.*
data class Rom(

View File

@ -0,0 +1,10 @@
package me.magnum.melonds.domain.model.rom.config
import java.util.*
data class RomConfig(
var runtimeConsoleType: RuntimeConsoleType = RuntimeConsoleType.DEFAULT,
var runtimeMicSource: RuntimeMicSource = RuntimeMicSource.DEFAULT,
var layoutId: UUID? = null,
val gbaSlotConfig: RomGbaSlotConfig = RomGbaSlotConfig.None,
)

View File

@ -0,0 +1,9 @@
package me.magnum.melonds.domain.model.rom.config
import android.net.Uri
sealed class RomGbaSlotConfig {
data object None : RomGbaSlotConfig()
data class GbaRom(val romPath: Uri?, val savePath: Uri?) : RomGbaSlotConfig()
data object MemoryExpansion : RomGbaSlotConfig()
}

View File

@ -1,4 +1,6 @@
package me.magnum.melonds.domain.model
package me.magnum.melonds.domain.model.rom.config
import me.magnum.melonds.domain.model.ConsoleType
enum class RuntimeConsoleType(val targetConsoleType: ConsoleType?) : RuntimeEnum<RuntimeConsoleType, ConsoleType> {
DEFAULT(null),

View File

@ -1,4 +1,4 @@
package me.magnum.melonds.domain.model
package me.magnum.melonds.domain.model.rom.config
interface RuntimeEnum<T, U> {
fun getDefault(): T

View File

@ -1,4 +1,6 @@
package me.magnum.melonds.domain.model
package me.magnum.melonds.domain.model.rom.config
import me.magnum.melonds.domain.model.MicSource
enum class RuntimeMicSource(val micSource: MicSource?) : RuntimeEnum<RuntimeMicSource, MicSource> {
DEFAULT(null),

View File

@ -3,8 +3,8 @@ package me.magnum.melonds.domain.repositories
import android.net.Uri
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.RomScanningStatus
import java.util.*

View File

@ -2,8 +2,7 @@ package me.magnum.melonds.domain.repositories
import android.graphics.Bitmap
import android.net.Uri
import io.reactivex.Single
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.SaveStateSlot
interface SaveStatesRepository {

View File

@ -5,6 +5,7 @@ import io.reactivex.Observable
import kotlinx.coroutines.flow.Flow
import me.magnum.melonds.domain.model.*
import me.magnum.melonds.domain.model.camera.DSiCameraSourceType
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.ui.Theme
import java.util.*

View File

@ -4,7 +4,7 @@ import android.net.Uri
import kotlinx.coroutines.flow.Flow
import me.magnum.melonds.domain.model.Cheat
import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.emulator.FirmwareLaunchResult
import me.magnum.melonds.domain.model.emulator.RomLaunchResult
import me.magnum.melonds.domain.model.retroachievements.GameAchievementData

View File

@ -13,13 +13,13 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.magnum.melonds.common.romprocessors.RomFileProcessorFactory
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.RomScanningStatus
import me.magnum.melonds.domain.repositories.RomsRepository
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.extensions.addTo
import me.magnum.melonds.impl.dtos.RomDto
import me.magnum.melonds.impl.dtos.rom.RomDto
import me.magnum.melonds.utils.FileUtils
import me.magnum.melonds.utils.SubjectSharedFlow
import java.io.File

View File

@ -4,7 +4,7 @@ import android.graphics.Bitmap
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.SaveStateSlot
import me.magnum.melonds.domain.repositories.SaveStatesRepository
import me.magnum.melonds.domain.repositories.SettingsRepository

View File

@ -5,7 +5,7 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.SizeUnit
import me.magnum.melonds.domain.repositories.SettingsRepository
import java.io.File

View File

@ -7,7 +7,7 @@ import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import me.magnum.melonds.common.romprocessors.RomFileProcessorFactory
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import java.io.File
import java.util.*
import java.util.concurrent.locks.ReentrantLock

View File

@ -5,7 +5,7 @@ import android.graphics.Bitmap
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.squareup.picasso.Picasso
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.SaveStateSlot
import java.io.File

View File

@ -36,7 +36,7 @@ import me.magnum.melonds.domain.model.LayoutConfiguration
import me.magnum.melonds.domain.model.MacAddress
import me.magnum.melonds.domain.model.MicSource
import me.magnum.melonds.domain.model.RendererConfiguration
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.domain.model.SaveStateLocation
import me.magnum.melonds.domain.model.SizeUnit

View File

@ -1,11 +1,10 @@
package me.magnum.melonds.impl.dtos
package me.magnum.melonds.impl.dtos.rom
import android.net.Uri
import com.google.gson.annotations.SerializedName
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import java.util.*
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
import java.util.UUID
data class RomConfigDto(
@SerializedName("runtimeConsoleType")
@ -14,12 +13,8 @@ data class RomConfigDto(
val runtimeMicSource: RuntimeMicSource,
@SerializedName("layoutId")
val layoutId: String?,
@SerializedName("loadGbaCart")
val loadGbaCart: Boolean,
@SerializedName("gbaCartPath")
val gbaCartPath: String?,
@SerializedName("gbaSavePath")
val gbaSavePath: String?,
@SerializedName("gbaSlotConfig")
val gbaSlotConfig: RomGbaSlotConfigDto,
) {
companion object {
@ -28,9 +23,7 @@ data class RomConfigDto(
romConfig.runtimeConsoleType,
romConfig.runtimeMicSource,
romConfig.layoutId?.toString(),
romConfig.loadGbaCart,
romConfig.gbaCartPath?.toString(),
romConfig.gbaSavePath?.toString(),
RomGbaSlotConfigDto.fromModel(romConfig.gbaSlotConfig),
)
}
}
@ -40,9 +33,7 @@ data class RomConfigDto(
runtimeConsoleType,
runtimeMicSource,
layoutId?.let { UUID.fromString(it) },
loadGbaCart,
gbaCartPath?.let { Uri.parse(it) },
gbaSavePath?.let { Uri.parse(it) },
gbaSlotConfig.toModel(),
)
}
}

View File

@ -1,8 +1,8 @@
package me.magnum.melonds.impl.dtos
package me.magnum.melonds.impl.dtos.rom
import android.net.Uri
import com.google.gson.annotations.SerializedName
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import java.util.*
data class RomDto(

View File

@ -0,0 +1,51 @@
package me.magnum.melonds.impl.dtos.rom
import android.net.Uri
import com.google.gson.annotations.SerializedName
import me.magnum.melonds.domain.model.rom.config.RomGbaSlotConfig
/**
* GBA slot config DTO holds information about all possible configs in a single object. This makes (de)serialization easier since it avoids dealing with polymorphism.
*/
data class RomGbaSlotConfigDto(
@SerializedName("type")
val type: Type,
@SerializedName("gbaRomPath")
val gbaRomPath: String?,
@SerializedName("gbaSavePath")
val gbaSavePath: String?,
) {
enum class Type {
None, GbaRom, MemoryExpansion
}
fun toModel(): RomGbaSlotConfig {
return when (type) {
Type.None -> RomGbaSlotConfig.None
Type.GbaRom -> RomGbaSlotConfig.GbaRom(
romPath = gbaRomPath?.let { Uri.parse(it) },
savePath = gbaSavePath?.let { Uri.parse(it) },
)
Type.MemoryExpansion -> RomGbaSlotConfig.MemoryExpansion
}
}
companion object {
fun fromModel(romGbaSlotConfig: RomGbaSlotConfig): RomGbaSlotConfigDto {
return RomGbaSlotConfigDto(
type = romGbaSlotConfig.dtoType(),
gbaRomPath = (romGbaSlotConfig as? RomGbaSlotConfig.GbaRom)?.romPath?.toString(),
gbaSavePath = (romGbaSlotConfig as? RomGbaSlotConfig.GbaRom)?.savePath?.toString(),
)
}
private fun RomGbaSlotConfig.dtoType(): Type {
return when (this) {
is RomGbaSlotConfig.None -> Type.None
is RomGbaSlotConfig.GbaRom -> Type.GbaRom
is RomGbaSlotConfig.MemoryExpansion -> Type.MemoryExpansion
}
}
}
}

View File

@ -18,14 +18,15 @@ import me.magnum.melonds.domain.model.Cheat
import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.EmulatorConfiguration
import me.magnum.melonds.domain.model.MicSource
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeEnum
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeEnum
import me.magnum.melonds.domain.model.emulator.FirmwareLaunchResult
import me.magnum.melonds.domain.model.emulator.RomLaunchResult
import me.magnum.melonds.domain.model.retroachievements.GameAchievementData
import me.magnum.melonds.domain.model.retroachievements.RAEvent
import me.magnum.melonds.domain.model.retroachievements.RASimpleAchievement
import me.magnum.melonds.domain.model.rom.config.RomGbaSlotConfig
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.domain.services.EmulatorManager
import me.magnum.melonds.impl.camera.DSiCameraSourceMultiplexer
@ -60,7 +61,20 @@ class AndroidEmulatorManager(
return@withContext RomLaunchResult.LaunchFailedSramProblem(exception)
}
val loadResult = MelonEmulator.loadRom(romUri, sram, rom.config.mustLoadGbaCart(), rom.config.gbaCartPath, rom.config.gbaSavePath)
val gbaSlotRomConfig = rom.config.gbaSlotConfig
val gbaSlotType = when (gbaSlotRomConfig) {
RomGbaSlotConfig.None -> MelonEmulator.GbaSlotType.NONE
is RomGbaSlotConfig.GbaRom -> MelonEmulator.GbaSlotType.GBA_ROM
RomGbaSlotConfig.MemoryExpansion -> MelonEmulator.GbaSlotType.MEMORY_EXPANSION
}
val loadResult = MelonEmulator.loadRom(
romUri = romUri,
sramUri = sram,
gbaSlotType = gbaSlotType,
gbaRomUri = (gbaSlotRomConfig as? RomGbaSlotConfig.GbaRom)?.romPath,
gbaSramUri = (gbaSlotRomConfig as? RomGbaSlotConfig.GbaRom)?.savePath
)
if (loadResult.isTerminal || !isActive) {
cameraManager.stopCurrentCameraSource()
RomLaunchResult.LaunchFailed(loadResult)

View File

@ -1,7 +1,7 @@
package me.magnum.melonds.impl.emulator
import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.emulator.EmulatorSessionUpdateAction
import me.magnum.melonds.domain.model.retroachievements.GameAchievementData

View File

@ -2,7 +2,7 @@ package me.magnum.melonds.impl.emulator
import android.net.Uri
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.repositories.SettingsRepository
class SramProvider(

View File

@ -0,0 +1,49 @@
package me.magnum.melonds.migrations
import me.magnum.melonds.migrations.helper.GenericJsonArrayMigrationHelper
import me.magnum.melonds.migrations.legacy.RomConfigDto25
import me.magnum.melonds.migrations.legacy.RomConfigDto31
import me.magnum.melonds.migrations.legacy.RomDto25
import me.magnum.melonds.migrations.legacy.RomDto31
import me.magnum.melonds.migrations.legacy.RomGbaSlotConfigDto31
class Migration30to31(
private val romMigrationHelper: GenericJsonArrayMigrationHelper,
) : Migration {
override val from = 30
override val to = 31
override fun migrate() {
romMigrationHelper.migrateJsonArrayData(ROM_DATA_FILE, RomDto25::class.java) {
RomDto31(
it.name,
it.developerName,
it.fileName,
it.uri,
it.parentTreeUri,
RomConfigDto31(
it.config.runtimeConsoleType,
it.config.runtimeMicSource,
it.config.layoutId,
createGbaSlotConfigDto(it.config),
),
it.lastPlayed,
it.isDsiWareTitle,
it.retroAchievementsHash,
)
}
}
private fun createGbaSlotConfigDto(oldConfig: RomConfigDto25): RomGbaSlotConfigDto31 {
return RomGbaSlotConfigDto31(
type = if (oldConfig.loadGbaCart) RomGbaSlotConfigDto31.Type.GbaRom else RomGbaSlotConfigDto31.Type.None,
gbaRomPath = oldConfig.gbaCartPath,
gbaSavePath = oldConfig.gbaSavePath,
)
}
companion object {
private const val ROM_DATA_FILE = "rom_data.json"
}
}

View File

@ -2,8 +2,8 @@ package me.magnum.melonds.migrations.legacy
import android.net.Uri
import com.google.gson.annotations.SerializedName
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
import java.util.*
data class RomConfig1(

View File

@ -1,9 +1,8 @@
package me.magnum.melonds.migrations.legacy
import com.google.gson.annotations.SerializedName
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import java.util.*
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
data class RomConfigDto25(
@SerializedName("runtimeConsoleType")

View File

@ -0,0 +1,16 @@
package me.magnum.melonds.migrations.legacy
import com.google.gson.annotations.SerializedName
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
data class RomConfigDto31(
@SerializedName("runtimeConsoleType")
val runtimeConsoleType: RuntimeConsoleType,
@SerializedName("runtimeMicSource")
val runtimeMicSource: RuntimeMicSource,
@SerializedName("layoutId")
val layoutId: String?,
@SerializedName("gbaSlotConfig")
val gbaSlotConfig: RomGbaSlotConfigDto31,
)

View File

@ -0,0 +1,28 @@
package me.magnum.melonds.migrations.legacy
import com.google.gson.annotations.SerializedName
import java.util.Date
/**
* ROM DTO used from app version 27.
*/
data class RomDto31(
@SerializedName("name")
val name: String,
@SerializedName("developerName")
val developerName: String,
@SerializedName("fileName")
val fileName: String,
@SerializedName("uri")
val uri: String,
@SerializedName("parentTreeUri")
val parentTreeUri: String,
@SerializedName("config")
var config: RomConfigDto31,
@SerializedName("lastPlayed")
var lastPlayed: Date? = null,
@SerializedName("isDsiWareTitle")
val isDsiWareTitle: Boolean,
@SerializedName("retroAchievementsHash")
val retroAchievementsHash: String,
)

View File

@ -0,0 +1,17 @@
package me.magnum.melonds.migrations.legacy
import com.google.gson.annotations.SerializedName
class RomGbaSlotConfigDto31(
@SerializedName("type")
val type: Type,
@SerializedName("gbaRomPath")
val gbaRomPath: String?,
@SerializedName("gbaSavePath")
val gbaSavePath: String?,
) {
enum class Type {
None, GbaRom, MemoryExpansion
}
}

View File

@ -2,11 +2,11 @@ package me.magnum.melonds.parcelables
import android.os.Parcel
import android.os.Parcelable
import androidx.core.net.toUri
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import java.util.*
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
import me.magnum.melonds.extensions.parcelable
import java.util.UUID
class RomConfigParcelable : Parcelable {
val romConfig: RomConfig
@ -16,22 +16,19 @@ class RomConfigParcelable : Parcelable {
}
private constructor(parcel: Parcel) {
romConfig = RomConfig()
romConfig.runtimeConsoleType = RuntimeConsoleType.entries[parcel.readInt()]
romConfig.runtimeMicSource = RuntimeMicSource.entries[parcel.readInt()]
romConfig.layoutId = parcel.readString()?.let { UUID.fromString(it) }
romConfig.loadGbaCart = parcel.readInt() == 1
romConfig.gbaCartPath = parcel.readString()?.toUri()
romConfig.gbaSavePath = parcel.readString()?.toUri()
romConfig = RomConfig(
runtimeConsoleType = RuntimeConsoleType.entries[parcel.readInt()],
runtimeMicSource = RuntimeMicSource.entries[parcel.readInt()],
layoutId = parcel.readString()?.let { UUID.fromString(it) },
gbaSlotConfig = parcel.parcelable<RomGbaSlotConfigParcelable>()!!.gbaSlotConfig,
)
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(romConfig.runtimeConsoleType.ordinal)
dest.writeInt(romConfig.runtimeMicSource.ordinal)
dest.writeString(romConfig.layoutId?.toString())
dest.writeInt(if (romConfig.loadGbaCart) 1 else 0)
dest.writeString(romConfig.gbaCartPath?.toString())
dest.writeString(romConfig.gbaSavePath?.toString())
dest.writeParcelable(RomGbaSlotConfigParcelable(romConfig.gbaSlotConfig), 0)
}
override fun describeContents(): Int {

View File

@ -0,0 +1,54 @@
package me.magnum.melonds.parcelables
import android.os.Parcel
import android.os.Parcelable
import androidx.core.net.toUri
import me.magnum.melonds.domain.model.rom.config.RomGbaSlotConfig
class RomGbaSlotConfigParcelable : Parcelable {
val gbaSlotConfig: RomGbaSlotConfig
constructor(gbaSlotConfig: RomGbaSlotConfig) {
this.gbaSlotConfig = gbaSlotConfig
}
private constructor(parcel: Parcel) {
val type = parcel.readInt()
gbaSlotConfig = when (type) {
TYPE_NONE -> RomGbaSlotConfig.None
TYPE_GBA_ROM -> RomGbaSlotConfig.GbaRom(parcel.readString()?.toUri(), parcel.readString()?.toUri())
TYPE_MEMORY_EXPANSION -> RomGbaSlotConfig.MemoryExpansion
else -> throw UnsupportedOperationException("Unsupported GBA slot type: $type")
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
when (gbaSlotConfig) {
is RomGbaSlotConfig.None -> parcel.writeInt(TYPE_NONE)
is RomGbaSlotConfig.GbaRom -> {
parcel.writeInt(TYPE_GBA_ROM)
parcel.writeString(gbaSlotConfig.romPath?.toString())
parcel.writeString(gbaSlotConfig.savePath?.toString())
}
is RomGbaSlotConfig.MemoryExpansion -> parcel.writeInt(TYPE_MEMORY_EXPANSION)
}
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<RomGbaSlotConfigParcelable> {
private const val TYPE_NONE = 0
private const val TYPE_GBA_ROM = 1
private const val TYPE_MEMORY_EXPANSION = 2
override fun createFromParcel(parcel: Parcel): RomGbaSlotConfigParcelable {
return RomGbaSlotConfigParcelable(parcel)
}
override fun newArray(size: Int): Array<RomGbaSlotConfigParcelable?> {
return arrayOfNulls(size)
}
}
}

View File

@ -3,7 +3,7 @@ package me.magnum.melonds.parcelables
import android.os.Parcel
import android.os.Parcelable
import androidx.core.net.toUri
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.extensions.parcelable
import java.util.*

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.retroachievements.RAUserAchievement
import me.magnum.melonds.domain.repositories.RetroAchievementsRepository
import me.magnum.melonds.domain.repositories.SettingsRepository

View File

@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.repositories.RomsRepository
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.impl.RomIconProvider

View File

@ -1,6 +1,6 @@
package me.magnum.melonds.ui.dsiwaremanager.model
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
sealed class DSiWareMangerRomListUiState {
object Loading : DSiWareMangerRomListUiState()

View File

@ -41,7 +41,7 @@ import me.magnum.melonds.common.Permission
import me.magnum.melonds.common.contracts.FilePickerContract
import me.magnum.melonds.domain.model.ConfigurationDirResult
import me.magnum.melonds.domain.model.DSiWareTitle
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.domain.model.dsinand.DSiWareTitleFileType
import me.magnum.melonds.ui.common.FabActionItem

View File

@ -43,8 +43,8 @@ import androidx.core.graphics.createBitmap
import androidx.core.graphics.set
import androidx.lifecycle.viewmodel.compose.viewModel
import me.magnum.melonds.R
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.ui.common.FullScreen
import me.magnum.melonds.ui.dsiwaremanager.DSiWareRomListViewModel

View File

@ -24,8 +24,8 @@ import androidx.compose.ui.unit.sp
import androidx.core.graphics.createBitmap
import androidx.core.graphics.set
import me.magnum.melonds.R
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.ui.common.component.text.CaptionText
import me.magnum.melonds.ui.romlist.RomIcon

View File

@ -69,7 +69,7 @@ import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.FpsCounterPosition
import me.magnum.melonds.domain.model.LayoutComponent
import me.magnum.melonds.domain.model.Orientation
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.SaveStateSlot
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.extensions.insetsControllerCompat

View File

@ -1,7 +1,7 @@
package me.magnum.melonds.ui.emulator
import dagger.hilt.android.lifecycle.HiltViewModel
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.repositories.RetroAchievementsRepository
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.impl.emulator.EmulatorSession

View File

@ -41,7 +41,7 @@ import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.FpsCounterPosition
import me.magnum.melonds.domain.model.LayoutConfiguration
import me.magnum.melonds.domain.model.Orientation
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.RomInfo
import me.magnum.melonds.domain.model.RuntimeBackground
import me.magnum.melonds.domain.model.SaveStateSlot

View File

@ -2,7 +2,7 @@ package me.magnum.melonds.ui.emulator.model
import me.magnum.melonds.MelonEmulator
import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
sealed class EmulatorState {
data object Uninitialized : EmulatorState()

View File

@ -34,7 +34,7 @@ class RomDetailsActivity : AppCompatActivity() {
val romRetroAchievementsViewModel by viewModels<RomDetailsRetroAchievementsViewModel>()
val rom by romDetailsViewModel.rom.collectAsState()
val romConfig by romDetailsViewModel.romConfig.collectAsState()
val romConfig by romDetailsViewModel.romConfigUiState.collectAsState()
val retroAchievementsUiState by romRetroAchievementsViewModel.uiState.collectAsState()

View File

@ -2,7 +2,7 @@ package me.magnum.melonds.ui.romdetails
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.repositories.RetroAchievementsRepository
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.parcelables.RomParcelable

View File

@ -1,11 +1,13 @@
package me.magnum.melonds.ui.romdetails
import android.content.Context
import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.rx2.awaitSingleOrNull
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.rom.config.RomGbaSlotConfig
import me.magnum.melonds.domain.repositories.LayoutsRepository
import me.magnum.melonds.ui.romdetails.model.RomConfigUiModel
import me.magnum.melonds.utils.FileUtils
import me.magnum.melonds.ui.romdetails.model.RomGbaSlotConfigUiModel
class RomDetailsUiMapper(
private val context: Context,
@ -18,9 +20,19 @@ class RomDetailsUiMapper(
runtimeMicSource = romConfig.runtimeMicSource,
layoutId = romConfig.layoutId,
layoutName = romConfig.layoutId?.let { layoutsRepository.getLayout(it).awaitSingleOrNull()?.name } ?: layoutsRepository.getGlobalLayoutPlaceholder().name,
loadGbaCart = romConfig.loadGbaCart,
gbaCartPath = FileUtils.getAbsolutePathFromSAFUri(context, romConfig.gbaCartPath),
gbaSavePath = FileUtils.getAbsolutePathFromSAFUri(context, romConfig.gbaSavePath),
gbaSlotConfig = mapGbaSlotConfigToUi(romConfig.gbaSlotConfig),
)
}
private fun mapGbaSlotConfigToUi(gbaSlotConfig: RomGbaSlotConfig): RomGbaSlotConfigUiModel {
return when (gbaSlotConfig) {
is RomGbaSlotConfig.None -> RomGbaSlotConfigUiModel(type = RomGbaSlotConfigUiModel.Type.None)
is RomGbaSlotConfig.GbaRom -> RomGbaSlotConfigUiModel(
type = RomGbaSlotConfigUiModel.Type.GbaRom,
gbaRomPath = gbaSlotConfig.romPath?.let { DocumentFile.fromSingleUri(context, it)?.name },
gbaSavePath = gbaSlotConfig.savePath?.let { DocumentFile.fromSingleUri(context, it)?.name },
)
is RomGbaSlotConfig.MemoryExpansion -> RomGbaSlotConfigUiModel(type = RomGbaSlotConfigUiModel.Type.MemoryExpansion)
}
}
}

View File

@ -6,18 +6,20 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.magnum.melonds.common.Permission
import me.magnum.melonds.common.UriPermissionManager
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.domain.model.rom.config.RomGbaSlotConfig
import me.magnum.melonds.domain.repositories.RomsRepository
import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.impl.RomIconProvider
import me.magnum.melonds.parcelables.RomParcelable
import me.magnum.melonds.ui.romdetails.model.RomConfigUiState
import me.magnum.melonds.ui.romdetails.model.RomConfigUpdateEvent
import me.magnum.melonds.ui.romdetails.model.RomGbaSlotConfigUiModel
import me.magnum.melonds.ui.romlist.RomIcon
import javax.inject.Inject
@ -34,25 +36,51 @@ class RomDetailsViewModel @Inject constructor(
private val _rom = MutableStateFlow(savedStateHandle.get<RomParcelable>(RomDetailsActivity.KEY_ROM)!!.rom)
val rom = _rom.asStateFlow()
private val _romConfig = MutableStateFlow<RomConfigUiState>(RomConfigUiState.Loading)
val romConfig by lazy {
updateRomConfigState()
_romConfig.asStateFlow()
private val _romConfig = MutableStateFlow(_rom.value.config)
val romConfigUiState by lazy {
val uiStateFlow = MutableStateFlow<RomConfigUiState>(RomConfigUiState.Loading)
viewModelScope.launch {
_romConfig.map {
romDetailsUiMapper.mapRomConfigToUi(it)
}.collect {
uiStateFlow.value = RomConfigUiState.Ready(it)
}
}
uiStateFlow.asStateFlow()
}
fun onRomConfigUpdateEvent(event: RomConfigUpdateEvent) {
val newConfig = when(event) {
is RomConfigUpdateEvent.RuntimeConsoleUpdate -> _rom.value.config.copy(runtimeConsoleType = event.newRuntimeConsole)
is RomConfigUpdateEvent.RuntimeMicSourceUpdate -> _rom.value.config.copy(runtimeMicSource = event.newRuntimeMicSource)
is RomConfigUpdateEvent.LayoutUpdate -> _rom.value.config.copy(layoutId = event.newLayoutId)
is RomConfigUpdateEvent.LoadGbaRomUpdate -> _rom.value.config.copy(loadGbaCart = event.shouldLoadRom)
is RomConfigUpdateEvent.GbaRomPathUpdate -> _rom.value.config.copy(gbaCartPath = event.gbaRomPath)
is RomConfigUpdateEvent.GbaSavePathUpdate -> _rom.value.config.copy(gbaSavePath = event.gbaSavePath)
val currentRomConfig = _romConfig.value
val newRomConfigUiModel = when(event) {
is RomConfigUpdateEvent.RuntimeConsoleUpdate -> currentRomConfig.copy(runtimeConsoleType = event.newRuntimeConsole)
is RomConfigUpdateEvent.RuntimeMicSourceUpdate -> currentRomConfig.copy(runtimeMicSource = event.newRuntimeMicSource)
is RomConfigUpdateEvent.LayoutUpdate -> currentRomConfig.copy(layoutId = event.newLayoutId)
is RomConfigUpdateEvent.GbaSlotTypeUpdated -> currentRomConfig.let {
val newGbaSlotConfig = when (event.type) {
RomGbaSlotConfigUiModel.Type.None -> RomGbaSlotConfig.None
RomGbaSlotConfigUiModel.Type.GbaRom -> RomGbaSlotConfig.GbaRom(null, null)
RomGbaSlotConfigUiModel.Type.MemoryExpansion -> RomGbaSlotConfig.MemoryExpansion
}
it.copy(gbaSlotConfig = newGbaSlotConfig)
}
is RomConfigUpdateEvent.GbaRomPathUpdate -> currentRomConfig.let {
(currentRomConfig.gbaSlotConfig as? RomGbaSlotConfig.GbaRom)?.let { gbaConfig ->
it.copy(gbaSlotConfig = gbaConfig.copy(romPath = event.gbaRomPath))
}
}
is RomConfigUpdateEvent.GbaSavePathUpdate -> currentRomConfig.let {
(currentRomConfig.gbaSlotConfig as? RomGbaSlotConfig.GbaRom)?.let { gbaConfig ->
it.copy(gbaSlotConfig = gbaConfig.copy(savePath = event.gbaSavePath))
}
}
}
_rom.update { it.copy(config = newConfig) }
saveRomConfig(newConfig)
updateRomConfigState()
newRomConfigUiModel?.let {
_romConfig.value = it
saveRomConfig(it)
}
}
suspend fun getRomIcon(rom: Rom): RomIcon {
@ -61,16 +89,11 @@ class RomDetailsViewModel @Inject constructor(
return RomIcon(romIconBitmap, iconFiltering)
}
private fun updateRomConfigState() {
viewModelScope.launch {
val romConfigUiModel = romDetailsUiMapper.mapRomConfigToUi(_rom.value.config)
_romConfig.value = RomConfigUiState.Ready(romConfigUiModel)
}
}
private fun saveRomConfig(newConfig: RomConfig) {
newConfig.gbaCartPath?.let { uriPermissionManager.persistFilePermissions(it, Permission.READ) }
newConfig.gbaSavePath?.let { uriPermissionManager.persistFilePermissions(it, Permission.READ_WRITE) }
if (newConfig.gbaSlotConfig is RomGbaSlotConfig.GbaRom) {
newConfig.gbaSlotConfig.romPath?.let { uriPermissionManager.persistFilePermissions(it, Permission.READ) }
newConfig.gbaSlotConfig.savePath?.let { uriPermissionManager.persistFilePermissions(it, Permission.READ_WRITE) }
}
romsRepository.updateRomConfig(_rom.value, newConfig)
}
}

View File

@ -1,18 +1,13 @@
package me.magnum.melonds.ui.romdetails.model
import android.net.Uri
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import java.util.*
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
import java.util.UUID
data class RomConfigUiModel(
val runtimeConsoleType: RuntimeConsoleType = RuntimeConsoleType.DEFAULT,
val runtimeMicSource: RuntimeMicSource = RuntimeMicSource.DEFAULT,
val layoutId: UUID? = null,
val layoutName: String? = null,
val loadGbaCart: Boolean = false,
val gbaCartPath: String? = null,
val gbaCartUri: Uri? = null,
val gbaSavePath: String? = null,
val gbaSaveUri: Uri? = null,
val gbaSlotConfig: RomGbaSlotConfigUiModel = RomGbaSlotConfigUiModel()
)

View File

@ -1,15 +1,15 @@
package me.magnum.melonds.ui.romdetails.model
import android.net.Uri
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
import java.util.UUID
sealed class RomConfigUpdateEvent {
data class RuntimeConsoleUpdate(val newRuntimeConsole: RuntimeConsoleType) : RomConfigUpdateEvent()
data class RuntimeMicSourceUpdate(val newRuntimeMicSource: RuntimeMicSource) : RomConfigUpdateEvent()
data class LayoutUpdate(val newLayoutId: UUID?) : RomConfigUpdateEvent()
data class LoadGbaRomUpdate(val shouldLoadRom: Boolean) : RomConfigUpdateEvent()
data class GbaSlotTypeUpdated(val type: RomGbaSlotConfigUiModel.Type) : RomConfigUpdateEvent()
data class GbaRomPathUpdate(val gbaRomPath: Uri?) : RomConfigUpdateEvent()
data class GbaSavePathUpdate(val gbaSavePath: Uri?) : RomConfigUpdateEvent()
}

View File

@ -0,0 +1,12 @@
package me.magnum.melonds.ui.romdetails.model
data class RomGbaSlotConfigUiModel(
val type: Type = Type.None,
val gbaRomPath: String? = null,
val gbaSavePath: String? = null,
) {
enum class Type {
None, GbaRom, MemoryExpansion
}
}

View File

@ -4,10 +4,14 @@ import android.app.Activity
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -17,18 +21,18 @@ import androidx.compose.ui.res.stringResource
import me.magnum.melonds.R
import me.magnum.melonds.common.Permission
import me.magnum.melonds.common.contracts.FilePickerContract
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.model.rom.config.RuntimeMicSource
import me.magnum.melonds.ui.common.MelonPreviewSet
import me.magnum.melonds.ui.common.preference.ActionLauncherItem
import me.magnum.melonds.ui.common.preference.SingleChoiceItem
import me.magnum.melonds.ui.common.preference.SwitchItem
import me.magnum.melonds.ui.layouts.LayoutSelectorActivity
import me.magnum.melonds.ui.romdetails.model.RomConfigUiModel
import me.magnum.melonds.ui.romdetails.model.RomConfigUiState
import me.magnum.melonds.ui.romdetails.model.RomConfigUpdateEvent
import me.magnum.melonds.ui.romdetails.model.RomGbaSlotConfigUiModel
import me.magnum.melonds.ui.theme.MelonTheme
import java.util.*
import java.util.UUID
@Composable
fun RomConfigUi(
@ -105,12 +109,15 @@ private fun Content(
}
)
SwitchItem(
name = stringResource(id = R.string.label_rom_config_load_gba_rom),
isOn = romConfig.loadGbaCart,
onToggle = {
onConfigUpdate(RomConfigUpdateEvent.LoadGbaRomUpdate(it))
},
val gbaSlotOptions = stringArrayResource(id = R.array.gba_slot_options)
SingleChoiceItem(
name = stringResource(id = R.string.label_rom_config_gba_slot),
value = gbaSlotOptions[romConfig.gbaSlotConfig.type.ordinal],
items = gbaSlotOptions.toList(),
selectedItemIndex = romConfig.gbaSlotConfig.type.ordinal,
onItemSelected = {
onConfigUpdate(RomConfigUpdateEvent.GbaSlotTypeUpdated(RomGbaSlotConfigUiModel.Type.entries[it]))
}
)
val gbaRomSelectorLauncher = rememberLauncherForActivityResult(FilePickerContract(Permission.READ)) { result ->
@ -118,28 +125,33 @@ private fun Content(
onConfigUpdate(RomConfigUpdateEvent.GbaRomPathUpdate(result))
}
}
ActionLauncherItem(
name = stringResource(id = R.string.label_rom_config_gba_rom_path),
value = romConfig.gbaCartPath ?: stringResource(id = R.string.not_set),
enabled = romConfig.loadGbaCart,
onLaunchAction = {
gbaRomSelectorLauncher.launch(Pair(romConfig.gbaCartUri, null))
}
)
val gbaSaveSelectorLauncher = rememberLauncherForActivityResult(FilePickerContract(Permission.READ_WRITE)) { result ->
if (result != null) {
onConfigUpdate(RomConfigUpdateEvent.GbaSavePathUpdate(result))
}
}
ActionLauncherItem(
name = stringResource(id = R.string.label_rom_config_gba_save_path),
value = romConfig.gbaSavePath ?: stringResource(id = R.string.not_set),
enabled = romConfig.loadGbaCart,
onLaunchAction = {
gbaSaveSelectorLauncher.launch(Pair(romConfig.gbaSaveUri, null))
AnimatedVisibility(visible = romConfig.gbaSlotConfig.type == RomGbaSlotConfigUiModel.Type.GbaRom) {
Column {
ActionLauncherItem(
name = stringResource(id = R.string.label_rom_config_gba_rom_path),
value = romConfig.gbaSlotConfig.gbaRomPath ?: stringResource(id = R.string.not_set),
enabled = true,
onLaunchAction = {
gbaRomSelectorLauncher.launch(Pair(null, null))
}
)
ActionLauncherItem(
name = stringResource(id = R.string.label_rom_config_gba_save_path),
value = romConfig.gbaSlotConfig.gbaSavePath ?: stringResource(id = R.string.not_set),
enabled = true,
onLaunchAction = {
gbaSaveSelectorLauncher.launch(Pair(null, null))
}
)
}
)
}
}
}
@ -152,6 +164,7 @@ private fun PreviewRomConfigUi() {
romConfigUiState = RomConfigUiState.Ready(
RomConfigUiModel(
layoutName = "Default",
gbaSlotConfig = RomGbaSlotConfigUiModel(type = RomGbaSlotConfigUiModel.Type.GbaRom)
),
),
onConfigUpdate = { },

View File

@ -10,8 +10,8 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import kotlinx.coroutines.launch
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.ui.common.MelonPreviewSet
import me.magnum.melonds.ui.romdetails.model.*
import me.magnum.melonds.ui.theme.MelonTheme

View File

@ -33,8 +33,8 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.pagerTabIndicatorOffset
import kotlinx.coroutines.launch
import me.magnum.melonds.R
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RomConfig
import me.magnum.melonds.ui.common.MelonPreviewSet
import me.magnum.melonds.ui.romdetails.model.RomDetailsTab
import me.magnum.melonds.ui.theme.MelonTheme

View File

@ -1,222 +0,0 @@
package me.magnum.melonds.ui.romlist
import android.Manifest
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import io.reactivex.disposables.Disposable
import me.magnum.melonds.R
import me.magnum.melonds.common.Permission
import me.magnum.melonds.databinding.DialogRomConfigBinding
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.RuntimeConsoleType
import me.magnum.melonds.domain.model.RuntimeMicSource
import me.magnum.melonds.extensions.setViewEnabledRecursive
import me.magnum.melonds.parcelables.RomConfigParcelable
import me.magnum.melonds.parcelables.RomParcelable
import me.magnum.melonds.ui.layouts.LayoutSelectorActivity
import me.magnum.melonds.common.contracts.FilePickerContract
import me.magnum.melonds.extensions.isMicrophonePermissionGranted
import me.magnum.melonds.extensions.parcelable
import me.magnum.melonds.utils.FileUtils
import java.util.*
class RomConfigDialog : DialogFragment() {
companion object {
private const val KEY_TITLE = "title"
private const val KEY_ROM = "rom"
private const val KEY_ROM_CONFIG = "rom_config"
fun newInstance(title: String, rom: Rom): RomConfigDialog {
return RomConfigDialog().apply {
arguments = bundleOf(
KEY_TITLE to title,
KEY_ROM to RomParcelable(rom)
)
}
}
}
private val romListViewModel: RomListViewModel by activityViewModels()
private lateinit var binding: DialogRomConfigBinding
private var layoutNameDisposable: Disposable? = null
private lateinit var rom: Rom
private lateinit var romConfig: RomConfig
private val layoutPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val layoutId = result.data?.getStringExtra(LayoutSelectorActivity.KEY_SELECTED_LAYOUT_ID)?.let { UUID.fromString(it) }
onLayoutIdSelected(layoutId)
}
}
private val gbaRomFilePicker = registerForActivityResult(FilePickerContract(Permission.READ)) {
if (it != null) {
onGbaRomPathSelected(it)
}
}
private val gbaSramFilePicker = registerForActivityResult(FilePickerContract(Permission.READ_WRITE)) {
if (it != null) {
onGbaSavePathSelected(it)
}
}
private val microphonePermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
onRuntimeMicSourceSelected(RuntimeMicSource.DEVICE)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ROM is immutable so we can use the arguments' one. Only the ROM config needs to be saved if the fragment is rebuilt
rom = arguments?.parcelable<RomParcelable>(KEY_ROM)?.rom ?: return
romConfig = if (savedInstanceState != null) {
savedInstanceState.parcelable<RomConfigParcelable>(KEY_ROM_CONFIG)?.romConfig ?: return
} else {
rom.config.copy()
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogRomConfigBinding.inflate(layoutInflater)
return AlertDialog.Builder(requireContext())
.setView(binding.root)
.setCancelable(true)
.create()
}
override fun onStart() {
super.onStart()
isCancelable = true
val title = arguments?.getString(KEY_TITLE)
binding.layoutPrefSystem.setOnClickListener {
AlertDialog.Builder(requireContext())
.setTitle(R.string.label_rom_config_console)
.setSingleChoiceItems(R.array.game_runtime_console_type_options, romConfig.runtimeConsoleType.ordinal) { dialog, which ->
val newConsoleType = RuntimeConsoleType.entries[which]
onRuntimeConsoleTypeSelected(newConsoleType)
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.cancel()
}
.show()
}
binding.layoutPrefRuntimeMicSource.setOnClickListener {
AlertDialog.Builder(requireContext())
.setTitle(R.string.microphone_source)
.setSingleChoiceItems(R.array.game_runtime_mic_source_options, romConfig.runtimeMicSource.ordinal) { dialog, which ->
val newMicSource = RuntimeMicSource.entries[which]
// Request mic permission if required
if (newMicSource == RuntimeMicSource.DEVICE && !requireContext().isMicrophonePermissionGranted()) {
microphonePermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
} else {
onRuntimeMicSourceSelected(newMicSource)
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.cancel()
}
.show()
}
binding.layoutPrefLayout.setOnClickListener {
val intent = Intent(requireContext(), LayoutSelectorActivity::class.java).apply {
putExtra(LayoutSelectorActivity.KEY_SELECTED_LAYOUT_ID, romConfig.layoutId?.toString())
}
layoutPickerLauncher.launch(intent)
}
binding.layoutPrefLoadGbaRom.setOnClickListener { binding.switchLoadGbaRom.toggle() }
binding.layoutPrefGbaRomPath.setOnClickListener {
gbaRomFilePicker.launch(Pair(romConfig.gbaCartPath, null))
}
layoutNameDisposable = romListViewModel.getLayout(romConfig.layoutId).subscribe { layout ->
binding.textPrefLayout.text = layout.name
}
binding.layoutPrefGbaSavePath.setOnClickListener {
gbaSramFilePicker.launch(Pair(romConfig.gbaSavePath, null))
}
binding.switchLoadGbaRom.setOnCheckedChangeListener { _, isChecked -> setLoadGbaRom(isChecked) }
binding.textRomConfigTitle.text = title
onRuntimeConsoleTypeSelected(romConfig.runtimeConsoleType)
onRuntimeMicSourceSelected(romConfig.runtimeMicSource)
binding.switchLoadGbaRom.isChecked = romConfig.loadGbaCart
binding.textPrefGbaRomPath.text = getUriPathOrDefault(romConfig.gbaCartPath)
binding.textPrefGbaSavePath.text = getUriPathOrDefault(romConfig.gbaSavePath)
binding.layoutPrefGbaRomPath.setViewEnabledRecursive(romConfig.loadGbaCart)
binding.layoutPrefGbaSavePath.setViewEnabledRecursive(romConfig.loadGbaCart)
binding.buttonRomConfigOk.setOnClickListener {
romListViewModel.updateRomConfig(rom, romConfig)
dismiss()
}
binding.buttonRomConfigCancel.setOnClickListener { dismiss() }
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(KEY_ROM_CONFIG, RomConfigParcelable(romConfig))
}
private fun setLoadGbaRom(loadGbaRom: Boolean) {
romConfig.loadGbaCart = loadGbaRom
binding.layoutPrefGbaRomPath.setViewEnabledRecursive(loadGbaRom)
binding.layoutPrefGbaSavePath.setViewEnabledRecursive(loadGbaRom)
}
private fun onRuntimeConsoleTypeSelected(consoleType: RuntimeConsoleType) {
val options = resources.getStringArray(R.array.game_runtime_console_type_options)
romConfig.runtimeConsoleType = consoleType
binding.textPrefRuntimeConsoleType.text = options[consoleType.ordinal]
}
private fun onRuntimeMicSourceSelected(micSource: RuntimeMicSource) {
val options = resources.getStringArray(R.array.game_runtime_mic_source_options)
romConfig.runtimeMicSource = micSource
binding.textPrefRuntimeMicSource.text = options[micSource.ordinal]
}
private fun onLayoutIdSelected(layoutId: UUID?) {
romConfig.layoutId = layoutId
layoutNameDisposable?.dispose()
layoutNameDisposable = romListViewModel.getLayout(layoutId).subscribe { layout ->
binding.textPrefLayout.text = layout.name
}
}
private fun onGbaRomPathSelected(romFileUri: Uri) {
romConfig.gbaCartPath = romFileUri
binding.textPrefGbaRomPath.text = getUriPathOrDefault(romFileUri)
}
private fun onGbaSavePathSelected(saveFileUri: Uri) {
romConfig.gbaSavePath = saveFileUri
binding.textPrefGbaSavePath.text = getUriPathOrDefault(saveFileUri)
}
private fun getUriPathOrDefault(uri: Uri?): String {
return FileUtils.getAbsolutePathFromSAFUri(requireContext(), uri) ?: getString(R.string.not_set)
}
override fun onStop() {
super.onStop()
layoutNameDisposable?.dispose()
}
}

View File

@ -29,7 +29,7 @@ import me.magnum.melonds.databinding.ActivityRomListBinding
import me.magnum.melonds.domain.model.ConfigurationDirResult
import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.DownloadProgress
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.SortingMode
import me.magnum.melonds.domain.model.Version
import me.magnum.melonds.domain.model.appupdate.AppUpdate

View File

@ -34,7 +34,7 @@ import me.magnum.melonds.R
import me.magnum.melonds.databinding.ItemRomConfigurableBinding
import me.magnum.melonds.databinding.ItemRomSimpleBinding
import me.magnum.melonds.databinding.RomListFragmentBinding
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.domain.model.RomScanningStatus
import me.magnum.melonds.extensions.setViewEnabledRecursive

View File

@ -7,14 +7,24 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import me.magnum.melonds.common.DirectoryAccessValidator
import me.magnum.melonds.common.Permission
import me.magnum.melonds.common.UriPermissionManager
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.*
import me.magnum.melonds.domain.model.ConfigurationDirResult
import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.LayoutConfiguration
import me.magnum.melonds.domain.model.SortingMode
import me.magnum.melonds.domain.model.SortingOrder
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.rom.config.RuntimeConsoleType
import me.magnum.melonds.domain.repositories.LayoutsRepository
import me.magnum.melonds.domain.repositories.RomsRepository
import me.magnum.melonds.domain.repositories.SettingsRepository
@ -24,7 +34,8 @@ import me.magnum.melonds.impl.RomIconProvider
import me.magnum.melonds.utils.EventSharedFlow
import me.magnum.melonds.utils.SubjectSharedFlow
import java.text.Normalizer
import java.util.*
import java.util.Calendar
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
@ -101,12 +112,6 @@ class RomListViewModel @Inject constructor(
romsRepository.rescanRoms()
}
fun updateRomConfig(rom: Rom, newConfig: RomConfig) {
newConfig.gbaCartPath?.let { uriPermissionManager.persistFilePermissions(it, Permission.READ) }
newConfig.gbaSavePath?.let { uriPermissionManager.persistFilePermissions(it, Permission.READ_WRITE) }
romsRepository.updateRomConfig(rom, newConfig)
}
fun getLayout(layoutId: UUID?): Single<LayoutConfiguration> {
return if (layoutId == null) {
Single.just(layoutsRepository.getGlobalLayoutPlaceholder())

View File

@ -16,7 +16,7 @@ import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import me.magnum.melonds.R
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.rom.Rom
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.ui.emulator.EmulatorActivity
import me.magnum.melonds.ui.romlist.RomIcon

View File

@ -1,231 +0,0 @@
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/layout_rom_config_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:orientation="horizontal"
android:paddingLeft="?attr/dialogPreferredPadding"
android:paddingRight="?attr/dialogPreferredPadding"
android:paddingTop="18dp"
android:paddingBottom="8dp"
app:layout_constraintTop_toTopOf="parent" >
<androidx.appcompat.widget.DialogTitle
android:id="@+id/textRomConfigTitle"
style="?android:attr/windowTitleStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
tools:text="Super Mario 64 DS"/>
</LinearLayout>
<ScrollView
android:id="@+id/layout_rom_config_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
android:orientation="vertical"
android:scrollbars="vertical"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/layoutPrefSystem"
style="@style/Layout.RomSetting">
<LinearLayout
style="@style/Layout.RomSettingContent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Title"
android:text="@string/label_rom_config_console"/>
<TextView
android:id="@+id/textPrefRuntimeConsoleType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Summary"
android:singleLine="true"
tools:text="Default" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPrefRuntimeMicSource"
style="@style/Layout.RomSetting">
<LinearLayout
style="@style/Layout.RomSettingContent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Title"
android:text="@string/microphone_source"/>
<TextView
android:id="@+id/textPrefRuntimeMicSource"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Summary"
android:singleLine="true"
tools:text="Default" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPrefLayout"
style="@style/Layout.RomSetting">
<LinearLayout
style="@style/Layout.RomSettingContent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Title"
android:text="@string/controller_layout" />
<TextView
android:id="@+id/textPrefLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Summary"
android:singleLine="true"
tools:text="Default" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPrefLoadGbaRom"
style="@style/Layout.RomSetting"
android:minHeight="48dp">
<RelativeLayout
style="@style/Layout.RomSettingContent">
<TextView
android:id="@+id/label_preference_load_gba_cart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Title"
android:text="@string/label_rom_config_load_gba_rom"/>
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchLoadGbaRom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:background="@null" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPrefGbaRomPath"
style="@style/Layout.RomSetting" >
<LinearLayout
style="@style/Layout.RomSettingContent">
<TextView
android:id="@+id/label_preference_gba_cart_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Title"
android:text="@string/label_rom_config_gba_rom_path"/>
<TextView
android:id="@+id/textPrefGbaRomPath"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Summary"
android:singleLine="true"
android:ellipsize="start"
tools:text="/storage/emulated/0/Emulators/GBA/Mario.gba" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/layoutPrefGbaSavePath"
style="@style/Layout.RomSetting" >
<LinearLayout
style="@style/Layout.RomSettingContent">
<TextView
android:id="@+id/label_preference_gba_save_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Title"
android:text="@string/label_rom_config_gba_save_path"/>
<TextView
android:id="@+id/textPrefGbaSavePath"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Text.RomSetting.Summary"
android:singleLine="true"
android:ellipsize="start"
tools:text="/storage/emulated/0/Emulators/GBA/Mario.sav" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/layout_rom_config_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom"
android:layoutDirection="locale"
android:orientation="horizontal"
android:paddingBottom="4dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="4dp">
<Space
android:id="@+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible"/>
<Button
android:id="@+id/button_rom_config_cancel"
style="?attr/buttonBarNegativeButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel" />
<Button
android:id="@+id/button_rom_config_ok"
style="?attr/buttonBarPositiveButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok" />
</LinearLayout>
</LinearLayout>

View File

@ -238,7 +238,6 @@
<string name="rom_settings">Configuración de Rom</string> <!-- Cadena de accesibilidad. No debe usar acrónimos -->
<string name="label_rom_config_console">Sistema de arranque</string>
<string name="label_rom_config_load_gba_rom">Cargar ROM de GBA</string>
<string name="label_rom_config_gba_rom_path">Ruta de acceso de la ROM de GBA</string>
<string name="label_rom_config_gba_save_path">Ruta de guardado de GBA</string>
<string name="rom_details_configuration_tab">Configuración</string>

View File

@ -239,7 +239,6 @@
<string name="rom_settings">Paramètres de la ROM</string>
<string name="label_rom_config_console">Démarrer le système</string>
<string name="label_rom_config_load_gba_rom">Charger la ROM GBA</string>
<string name="label_rom_config_gba_rom_path">Chemin de la ROM GBA</string>
<string name="label_rom_config_gba_save_path">Chemin de la sauvegarde GBA</string>
<string name="rom_details_configuration_tab">Configuration</string>

View File

@ -184,7 +184,6 @@
<string name="rom_settings">Pengaturan rom</string> <!-- Accessibility string. Should not use acronyms -->
<string name="label_rom_config_console">Boot sistem</string>
<string name="label_rom_config_load_gba_rom">Muat ROM GBA</string>
<string name="label_rom_config_gba_rom_path">Direktori ROM GBA</string>
<string name="label_rom_config_gba_save_path">Direktori simpanan GBA</string>

View File

@ -218,7 +218,6 @@
<string name="rom_settings">Configurações da ROM</string> <!-- Cadeia de caracteres de acessibilidade. Não se deve usar acrônimos (siglas) -->
<string name="label_rom_config_console">Sistema Inicializador</string>
<string name="label_rom_config_load_gba_rom">Carregar ROM de GBA</string>
<string name="label_rom_config_gba_rom_path">Caminho da ROM de GBA</string>
<string name="label_rom_config_gba_save_path">Caminho do arquivo de salvamento GBA</string>

View File

@ -244,7 +244,6 @@
<string name="rom_settings">Настройки игры</string> <!-- Accessibility string. Should not use acronyms -->
<string name="label_rom_config_console">Система загрузки</string>
<string name="label_rom_config_load_gba_rom">Загрузить образ GBA</string>
<string name="label_rom_config_gba_rom_path">Путь к образу GBA</string>
<string name="label_rom_config_gba_save_path">Путь к сохранению GBA</string>
<string name="rom_details_configuration_tab">Конфигурация</string>

View File

@ -252,7 +252,7 @@
<string name="rom_settings">Rom settings</string> <!-- Accessibility string. Should not use acronyms -->
<string name="label_rom_config_console">Boot system</string>
<string name="label_rom_config_load_gba_rom">Load GBA ROM</string>
<string name="label_rom_config_gba_slot">GBA slot</string>
<string name="label_rom_config_gba_rom_path">GBA ROM path</string>
<string name="label_rom_config_gba_save_path">GBA save path</string>
<string name="rom_details_configuration_tab">Configuration</string>
@ -390,6 +390,12 @@
<item>Device microphone</item>
</string-array>
<string-array name="gba_slot_options">
<item>None</item>
<item>GBA ROM</item>
<item>Memory expansion</item>
</string-array>
<string-array name="console_type_options">
<item>@string/console_ds</item>
<item>@string/console_dsi</item>

View File

@ -4,6 +4,6 @@ object AppConfig {
const val minSdkVersion = 24
const val ndkVersion = "25.1.8937393"
const val versionCode = 30
const val versionCode = 31
const val versionName = "Beta 1.9.3"
}

@ -1 +1 @@
Subproject commit fe50a17109d18f20459e6fb8fb70a87bc8375c7b
Subproject commit dc44fa17f9160798772cd22fcce9d3450250e2af