Skip to content
Open
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
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ android {
minSdk = 26
targetSdk = 28

versionCode = 4
versionCode = 5
versionName = "0.5.0"

buildConfigField("boolean", "GOLD", "false")
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@
android:host="pluvia"
android:scheme="home" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="config"
android:scheme="gamenative" />
</intent-filter>
<!-- TODO: Enable android:autoVerify once assetlinks.json is deployed at https://gamenative.app/.well-known/assetlinks.json -->
<intent-filter
><!--android:autoVerify="true"-->
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="https"
android:host="gamenative.app"
android:pathPrefix="/config" />
</intent-filter>
<intent-filter>
<action android:name="app.gamenative.LAUNCH_GAME" />
<category android:name="android.intent.category.DEFAULT" />
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/app/gamenative/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import app.gamenative.ui.PluviaMain
import app.gamenative.ui.enums.Orientation
import app.gamenative.utils.AnimatedPngDecoder
import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.ContainerConfigIO
import app.gamenative.utils.IconDecoder
import app.gamenative.utils.IntentLaunchManager
import com.posthog.PostHog
Expand Down Expand Up @@ -187,6 +188,28 @@ class MainActivity : ComponentActivity() {
private fun handleLaunchIntent(intent: Intent) {
Timber.d("[IntentLaunch]: handleLaunchIntent called with action=${intent.action}")
try {
// Handle deep link config import (gamenative://config?data=... or https://gamenative.app/config?data=...)
if (intent.action == Intent.ACTION_VIEW && intent.data != null) {
val uri = intent.data!!
val isConfigLink = (uri.scheme == "gamenative" && uri.host == "config") ||
(uri.scheme == "https" && uri.host == "gamenative.app" && uri.path?.startsWith("/config") == true)

if (isConfigLink) {
Timber.d("[ConfigImport]: Received config import link: ${uri.scheme}://${uri.host}${uri.path}")
val containerData = ContainerConfigIO.importFromDeepLink(uri)
if (containerData != null) {
Timber.i("[ConfigImport]: Successfully imported config from link: ${containerData.name}")
// Store imported config for user to apply
// You can emit an event here to show a dialog or navigate to container creation
PluviaApp.events.emit(AndroidEvent.ConfigImported(containerData))
} else {
Timber.e("[ConfigImport]: Failed to parse config from link")
}
return
}
}

// Handle game launch intent
val launchRequest = IntentLaunchManager.parseLaunchIntent(intent)
if (launchRequest != null) {
Timber.d("[IntentLaunch]: Received external launch intent for app ${launchRequest.appId}")
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/app/gamenative/events/AndroidEvent.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.gamenative.events

import app.gamenative.ui.enums.Orientation
import com.winlator.container.ContainerData
import java.util.EnumSet

interface AndroidEvent<T> : Event<T> {
Expand All @@ -18,5 +19,6 @@ interface AndroidEvent<T> : Event<T> {
data class ShowGameFeedback(val appId: String) : AndroidEvent<Unit>
data class ShowLaunchingOverlay(val appName: String) : AndroidEvent<Unit>
data object HideLaunchingOverlay : AndroidEvent<Unit>
data class ConfigImported(val containerData: ContainerData) : AndroidEvent<Unit>
// data class SetAppBarVisibility(val visible: Boolean) : AndroidEvent<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package app.gamenative.ui.component.dialog

import android.content.Intent
import android.widget.Toast
import android.widget.Spinner
import android.widget.ArrayAdapter
import android.content.res.Configuration
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
Expand All @@ -15,6 +19,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
Expand All @@ -25,7 +30,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ViewList
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileDownload
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.AddCircleOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CenterAlignedTopAppBar
Expand Down Expand Up @@ -76,6 +84,8 @@ import app.gamenative.ui.theme.PluviaTheme
import app.gamenative.ui.theme.settingsTileColors
import app.gamenative.ui.theme.settingsTileColorsAlt
import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.ContainerConfigIO
import app.gamenative.utils.ContainerConfigIO.toTempContainer
import app.gamenative.service.SteamService
import com.winlator.contents.ContentProfile
import com.winlator.contents.ContentsManager
Expand Down Expand Up @@ -133,6 +143,45 @@ fun ContainerConfigDialog(
mutableStateOf(initialConfig)
}

// Import/Export state
var showImportDialog by remember { mutableStateOf(false) }
var showExportDialog by remember { mutableStateOf(false) }
var showImportMenu by remember { mutableStateOf(false) }
var showShareCodeDialog by remember { mutableStateOf(false) }
var shareCodeInput by remember { mutableStateOf("") }

// File picker for import
val importLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
val imported = ContainerConfigIO.importFromFile(context, it)
if (imported != null) {
config = imported
Toast.makeText(context, "Configuration imported successfully", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, context.getString(R.string.failed_to_import_config), Toast.LENGTH_LONG).show()
}
}
}

// File picker for export
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let {
// Create a temporary container to export current config
val tempContainer = config.toTempContainer()

val success = ContainerConfigIO.exportToFile(context, tempContainer, it)
if (success) {
Toast.makeText(context, "Configuration exported successfully", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to export configuration", Toast.LENGTH_LONG).show()
}
}
}

val screenSizes = stringArrayResource(R.array.screen_size_entries).toList()
val baseGraphicsDrivers = stringArrayResource(R.array.graphics_driver_entries).toList()
var graphicsDrivers by remember { mutableStateOf(baseGraphicsDrivers.toMutableList()) }
Expand Down Expand Up @@ -656,6 +705,64 @@ fun ContainerConfigDialog(
},
)
}

// Share Code Import Dialog
if (showShareCodeDialog) {
AlertDialog(
onDismissRequest = {
showShareCodeDialog = false
shareCodeInput = ""
},
title = { Text("Import from Share Code") },
text = {
Column {
Text("Paste the share code or link you received:")
Spacer(modifier = Modifier.padding(4.dp))
Text(
text = "Accepts: GN1:..., gamenative://..., or https://gamenative.app/...",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.padding(8.dp))
OutlinedTextField(
value = shareCodeInput,
onValueChange = { shareCodeInput = it },
label = { Text("Share Code or Link") },
placeholder = { Text("GN1:... or https://...") },
modifier = Modifier.fillMaxWidth(),
singleLine = false,
maxLines = 6
)
}
},
confirmButton = {
TextButton(
onClick = {
val imported = ContainerConfigIO.importFromShareCode(shareCodeInput)
if (imported != null) {
config = imported
Toast.makeText(context, "Configuration imported successfully", Toast.LENGTH_SHORT).show()
showShareCodeDialog = false
shareCodeInput = ""
} else {
Toast.makeText(context, "Invalid share code or link", Toast.LENGTH_LONG).show()
}
},
enabled = shareCodeInput.isNotBlank()
) {
Text("Import")
}
},
dismissButton = {
TextButton(onClick = {
showShareCodeDialog = false
shareCodeInput = ""
}) {
Text("Cancel")
}
}
)
}

Dialog(
onDismissRequest = onDismissCheck,
Expand Down Expand Up @@ -684,6 +791,69 @@ fun ContainerConfigDialog(
)
},
actions = {
// Import configuration button with dropdown menu
androidx.compose.foundation.layout.Box {
TextButton(
onClick = { showImportMenu = true },
) {
Icon(
imageVector = Icons.Default.FileOpen,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Import")
}
DropdownMenu(
expanded = showImportMenu,
onDismissRequest = { showImportMenu = false }
) {
DropdownMenuItem(
text = { Text("From File") },
onClick = {
showImportMenu = false
importLauncher.launch("application/json")
}
)
DropdownMenuItem(
text = { Text("From Share Code") },
onClick = {
showImportMenu = false
showShareCodeDialog = true
}
)
}
}
// Export configuration button
TextButton(
onClick = {
val filename = ContainerConfigIO.generateExportFilename(title.ifEmpty { "config" })
exportLauncher.launch(filename)
},
) {
Icon(
imageVector = Icons.Default.FileDownload,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Export")
}
// Share configuration button
IconButton(
onClick = {
// Create a temporary container to share current config
val tempContainer = config.toTempContainer()
val shareIntent = ContainerConfigIO.createShareMessageIntent(tempContainer, title)
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share_config)))
},
) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = stringResource(R.string.share_config)
)
}
// Save button
IconButton(
onClick = { onSave(config) },
content = { Icon(Icons.Default.Save, null) },
Expand Down
Loading