Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.gamenative.ui.screen.library.appscreen
import android.content.Context
import android.content.Intent
import app.gamenative.ui.util.SnackbarManager
import app.gamenative.ui.util.ContainerConfigTransfer
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
Expand All @@ -20,6 +21,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import app.gamenative.PluviaApp
Expand All @@ -37,10 +39,12 @@ import app.gamenative.utils.SteamGridDB
import app.gamenative.utils.createPinnedShortcut
import com.winlator.container.ContainerData
import java.io.File
import kotlin.text.Charsets
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* Abstract base class for AppScreen implementations.
Expand All @@ -50,6 +54,8 @@ abstract class BaseAppScreen {
// Shared state for install dialog - map of appId (String) to MessageDialogState
companion object {
private val installDialogStates = mutableStateMapOf<String, app.gamenative.ui.component.dialog.state.MessageDialogState>()
private val exportConfigRequests = mutableStateMapOf<String, Boolean>()
private val importConfigRequests = mutableStateMapOf<String, Boolean>()

fun showInstallDialog(appId: String, state: app.gamenative.ui.component.dialog.state.MessageDialogState) {
installDialogStates[appId] = state
Expand All @@ -62,6 +68,30 @@ abstract class BaseAppScreen {
fun getInstallDialogState(appId: String): app.gamenative.ui.component.dialog.state.MessageDialogState? {
return installDialogStates[appId]
}

fun requestExportConfig(appId: String) {
exportConfigRequests[appId] = true
}

fun clearExportConfigRequest(appId: String) {
exportConfigRequests.remove(appId)
}

fun shouldExportConfig(appId: String): Boolean {
return exportConfigRequests[appId] == true
}

fun requestImportConfig(appId: String) {
importConfigRequests[appId] = true
}

fun clearImportConfigRequest(appId: String) {
importConfigRequests.remove(appId)
}

fun shouldImportConfig(appId: String): Boolean {
return importConfigRequests[appId] == true
}
}

/**
Expand Down Expand Up @@ -248,6 +278,56 @@ abstract class BaseAppScreen {
)
}

/**
* Get export-config menu option. Subclasses can override to customize behavior
* or disable export-config entirely by returning null.
*/
@Composable
protected open fun getExportConfigOption(
context: Context,
libraryItem: LibraryItem,
): AppMenuOption? {
return AppMenuOption(
optionType = AppOptionMenuType.ExportConfig,
onClick = {
requestExportConfig(libraryItem.appId)
},
)
}

@Composable
protected open fun getImportConfigOption(
context: Context,
libraryItem: LibraryItem,
): AppMenuOption? {
return AppMenuOption(
optionType = AppOptionMenuType.ImportConfig,
onClick = {
requestImportConfig(libraryItem.appId)
},
)
}

/**
* Get config-related menu options (e.g. Export config, Import config).
* By default returns only Export config when supported; sources can override
* to add Import config or other options so they appear grouped together.
*/
@Composable
protected open fun getConfigMenuOptions(
context: Context,
libraryItem: LibraryItem,
): List<AppMenuOption> {
return if (supportsContainerConfig()) {
listOfNotNull(
getExportConfigOption(context, libraryItem),
getImportConfigOption(context, libraryItem),
)
} else {
emptyList()
}
}

/**
* Get Create Shortcut menu option. Subclasses can override to customize behavior.
*/
Expand Down Expand Up @@ -470,6 +550,13 @@ abstract class BaseAppScreen {
// Add any source-specific options
menuOptions.addAll(getSourceSpecificMenuOptions(context, libraryItem, onEditContainer, onBack, onClickPlay, isInstalled))

// Add config-related options (export/import) after source-specific options,
// so container-related items appear as:
// Reset Container, Reset DRM, Use Known Config, Export Config, Import Config.
if (isInstalled) {
menuOptions.addAll(getConfigMenuOptions(context, libraryItem))
}

return menuOptions
}

Expand All @@ -496,6 +583,7 @@ abstract class BaseAppScreen {
) {
val context = LocalContext.current
val displayInfo = getGameDisplayInfo(context, libraryItem)
val appId = libraryItem.appId

// Use composable state for values that change over time
var isInstalledState by remember(libraryItem.appId) {
Expand Down Expand Up @@ -576,6 +664,88 @@ abstract class BaseAppScreen {
},
)

var exportConfigRequested by remember(appId) {
mutableStateOf(shouldExportConfig(appId))
}

LaunchedEffect(appId) {
snapshotFlow { shouldExportConfig(appId) }
.collect { shouldRequest ->
exportConfigRequested = shouldRequest
}
}

val exportConfigLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json"),
) { uri ->
if (uri == null) {
clearExportConfigRequest(appId)
return@rememberLauncherForActivityResult
}

uiScope.launch {
try {
ContainerConfigTransfer.exportConfig(
context = context,
appId = appId,
uri = uri,
)
} finally {
clearExportConfigRequest(appId)
}
}
}

LaunchedEffect(exportConfigRequested) {
if (exportConfigRequested) {
val gameName = displayInfo.name.ifBlank { "game" }
val suggestedFileName = "${gameName}_config.json"
exportConfigLauncher.launch(suggestedFileName)
}
}

var importConfigRequested by remember(appId) {
mutableStateOf(shouldImportConfig(appId))
}

LaunchedEffect(appId) {
snapshotFlow { shouldImportConfig(appId) }
.collect { shouldRequest ->
importConfigRequested = shouldRequest
}
}

val importConfigLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
) { uri ->
if (uri == null) {
clearImportConfigRequest(appId)
return@rememberLauncherForActivityResult
}

uiScope.launch {
try {
ContainerConfigTransfer.importConfig(
context = context,
appId = appId,
uri = uri,
)
} finally {
clearImportConfigRequest(appId)
}
}
}

LaunchedEffect(importConfigRequested) {
if (importConfigRequested) {
importConfigLauncher.launch(
arrayOf("application/json", "text/json", "text/plain"),
)
}
}

val optionsMenu = getOptionsMenu(context, libraryItem, onEditContainer, onBack, onClickPlay, onTestGraphics, exportFrontendLauncher)

// Get download info based on game source for progress tracking
Expand Down
Loading