Add ability to choose game when hosting
Also fix bottom sheets so they survive rotation
This commit is contained in:
1 parent
68e9fd6cde
commit
6655672ee3
9 files changed
+267
-14
No files matched your search
@@ -141,6 +141,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.profileinstaller)
|
implementation(libs.androidx.profileinstaller)
|
||||||
|
|
||||||
// Kotlin extensions for lifecycle components
|
// Kotlin extensions for lifecycle components
|
||||||
|
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|
||||||
@@ -151,6 +152,7 @@ dependencies {
|
|||||||
|
|
||||||
// For loading game covers from disk and GameTDB
|
// For loading game covers from disk and GameTDB
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
// For loading custom GPU drivers
|
// For loading custom GPU drivers
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
@@ -164,6 +166,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.compose.material.icons)
|
implementation(libs.androidx.compose.material.icons)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.compose.runtime.livedata)
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling.preview)
|
debugImplementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
|||||||
+5
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.runningFold
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||||
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
||||||
@@ -153,6 +154,8 @@ class NetplaySession(
|
|||||||
|
|
||||||
fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer)
|
fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer)
|
||||||
|
|
||||||
|
fun changeGame(gameFile: GameFile) = nativeChangeGame(gameFile)
|
||||||
|
|
||||||
fun startGame() = nativeStartGame()
|
fun startGame() = nativeStartGame()
|
||||||
|
|
||||||
fun consumeBootSessionData(): Long {
|
fun consumeBootSessionData(): Long {
|
||||||
@@ -232,6 +235,8 @@ class NetplaySession(
|
|||||||
|
|
||||||
private external fun nativeReleaseBootSessionData(pointer: Long)
|
private external fun nativeReleaseBootSessionData(pointer: Long)
|
||||||
|
|
||||||
|
private external fun nativeChangeGame(gameFile: GameFile)
|
||||||
|
|
||||||
private external fun nativeStartGame()
|
private external fun nativeStartGame()
|
||||||
|
|
||||||
// NetPlayUI callbacks
|
// NetPlayUI callbacks
|
||||||
|
|||||||
+34
@@ -4,17 +4,22 @@ package org.dolphinemu.dolphinemu.features.netplay.model
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
|
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
|
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
|
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||||
|
|
||||||
class NetplayViewModel(
|
class NetplayViewModel(
|
||||||
private val netplaySession: NetplaySession,
|
private val netplaySession: NetplaySession,
|
||||||
@@ -41,10 +46,24 @@ class NetplayViewModel(
|
|||||||
private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int)
|
private val _maxBuffer = MutableStateFlow(IntSetting.NETPLAY_CLIENT_BUFFER_SIZE.int)
|
||||||
val maxBuffer = _maxBuffer.asStateFlow()
|
val maxBuffer = _maxBuffer.asStateFlow()
|
||||||
|
|
||||||
|
val gameFiles = GameFileCacheManager.getGameFiles().asFlow()
|
||||||
|
.map { it.toList() }
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(),
|
||||||
|
GameFileCacheManager.getGameFiles().value?.toList() ?: emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
val saveTransferProgress = netplaySession.saveTransferProgress
|
val saveTransferProgress = netplaySession.saveTransferProgress
|
||||||
|
|
||||||
val gameDigestProgress = netplaySession.gameDigestProgress
|
val gameDigestProgress = netplaySession.gameDigestProgress
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (netplaySession.isHosting) {
|
||||||
|
setInitialGame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startGame() {
|
fun startGame() {
|
||||||
netplaySession.startGame()
|
netplaySession.startGame()
|
||||||
}
|
}
|
||||||
@@ -64,6 +83,21 @@ class NetplayViewModel(
|
|||||||
netplaySession.adjustPadBufferSize(buffer)
|
netplaySession.adjustPadBufferSize(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeGame(gameFile: GameFile) {
|
||||||
|
StringSetting.NETPLAY_GAME.setString(NativeConfig.LAYER_BASE, gameFile.getGameId())
|
||||||
|
netplaySession.changeGame(gameFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInitialGame() {
|
||||||
|
val game = gameFiles.value
|
||||||
|
.find { it.getGameId() == StringSetting.NETPLAY_GAME.string }
|
||||||
|
?: gameFiles.value.firstOrNull()
|
||||||
|
|
||||||
|
if (game != null) {
|
||||||
|
changeGame(game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
|||||||
+3
@@ -9,6 +9,7 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
import androidx.lifecycle.flowWithLifecycle
|
||||||
@@ -53,6 +54,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
game = viewModel.game.collectAsState().value,
|
game = viewModel.game.collectAsState().value,
|
||||||
isHosting = viewModel.isHosting,
|
isHosting = viewModel.isHosting,
|
||||||
onStartGame = viewModel::startGame,
|
onStartGame = viewModel::startGame,
|
||||||
|
onGameSelected = viewModel::changeGame,
|
||||||
|
gameFiles = viewModel.gameFiles.collectAsState().value,
|
||||||
players = viewModel.players.collectAsState().value,
|
players = viewModel.players.collectAsState().value,
|
||||||
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
|
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
|
||||||
maxBuffer = viewModel.maxBuffer.collectAsState().value,
|
maxBuffer = viewModel.maxBuffer.collectAsState().value,
|
||||||
|
|||||||
+201
-12
@@ -3,12 +3,14 @@
|
|||||||
package org.dolphinemu.dolphinemu.features.netplay.ui
|
package org.dolphinemu.dolphinemu.features.netplay.ui
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -19,6 +21,9 @@ import androidx.compose.foundation.layout.statusBarsPadding
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
@@ -30,6 +35,7 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Remove
|
import androidx.compose.material.icons.filled.Remove
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -42,9 +48,10 @@ import androidx.compose.material3.MediumTopAppBar
|
|||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -55,6 +62,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -66,8 +75,11 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import org.dolphinemu.dolphinemu.R
|
import org.dolphinemu.dolphinemu.R
|
||||||
@@ -75,10 +87,12 @@ import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
|||||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||||
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
||||||
|
import org.dolphinemu.dolphinemu.model.GameFile
|
||||||
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
|
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
|
||||||
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
|
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
|
||||||
import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox
|
import org.dolphinemu.dolphinemu.ui.theme.OutlinedBox
|
||||||
import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme
|
import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme
|
||||||
|
import org.dolphinemu.dolphinemu.utils.CoilUtils
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -91,6 +105,8 @@ fun NetplayScreen(
|
|||||||
onSendMessage: (String) -> Unit,
|
onSendMessage: (String) -> Unit,
|
||||||
game: String,
|
game: String,
|
||||||
onStartGame: () -> Unit,
|
onStartGame: () -> Unit,
|
||||||
|
onGameSelected: (GameFile) -> Unit,
|
||||||
|
gameFiles: List<GameFile>,
|
||||||
hostInputAuthorityEnabled: Boolean,
|
hostInputAuthorityEnabled: Boolean,
|
||||||
maxBuffer: Int,
|
maxBuffer: Int,
|
||||||
onMaxBufferChanged: (Int) -> Unit,
|
onMaxBufferChanged: (Int) -> Unit,
|
||||||
@@ -125,12 +141,21 @@ fun NetplayScreen(
|
|||||||
.consumeWindowInsets(innerPadding)
|
.consumeWindowInsets(innerPadding)
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
|
|
||||||
|
var showChat by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var showGamePicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
LandscapeContent(
|
LandscapeContent(
|
||||||
isHosting = isHosting,
|
isHosting = isHosting,
|
||||||
messages = messages,
|
messages = messages,
|
||||||
onSendMessage = onSendMessage,
|
onSendMessage = onSendMessage,
|
||||||
|
showChat = showChat,
|
||||||
|
onShowChatChanged = { showChat = it },
|
||||||
game = game,
|
game = game,
|
||||||
|
gameFiles = gameFiles,
|
||||||
|
onGameSelected = onGameSelected,
|
||||||
|
showGamePicker = showGamePicker,
|
||||||
|
onShowGamePickerChanged = { showGamePicker = it },
|
||||||
players = players,
|
players = players,
|
||||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||||
maxBuffer = maxBuffer,
|
maxBuffer = maxBuffer,
|
||||||
@@ -142,7 +167,13 @@ fun NetplayScreen(
|
|||||||
isHosting = isHosting,
|
isHosting = isHosting,
|
||||||
messages = messages,
|
messages = messages,
|
||||||
onSendMessage = onSendMessage,
|
onSendMessage = onSendMessage,
|
||||||
|
showChat = showChat,
|
||||||
|
onShowChatChanged = { showChat = it },
|
||||||
game = game,
|
game = game,
|
||||||
|
gameFiles = gameFiles,
|
||||||
|
onGameSelected = onGameSelected,
|
||||||
|
showGamePicker = showGamePicker,
|
||||||
|
onShowGamePickerChanged = { showGamePicker = it },
|
||||||
players = players,
|
players = players,
|
||||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||||
maxBuffer = maxBuffer,
|
maxBuffer = maxBuffer,
|
||||||
@@ -201,7 +232,13 @@ private fun PortraitContent(
|
|||||||
isHosting: Boolean,
|
isHosting: Boolean,
|
||||||
messages: List<NetplayMessage>,
|
messages: List<NetplayMessage>,
|
||||||
onSendMessage: (String) -> Unit,
|
onSendMessage: (String) -> Unit,
|
||||||
|
showChat: Boolean,
|
||||||
|
onShowChatChanged: (Boolean) -> Unit,
|
||||||
game: String,
|
game: String,
|
||||||
|
gameFiles: List<GameFile>,
|
||||||
|
onGameSelected: (GameFile) -> Unit,
|
||||||
|
showGamePicker: Boolean,
|
||||||
|
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||||
players: List<Player>,
|
players: List<Player>,
|
||||||
hostInputAuthorityEnabled: Boolean,
|
hostInputAuthorityEnabled: Boolean,
|
||||||
maxBuffer: Int,
|
maxBuffer: Int,
|
||||||
@@ -215,6 +252,8 @@ private fun PortraitContent(
|
|||||||
Chat(
|
Chat(
|
||||||
messages = messages,
|
messages = messages,
|
||||||
onSendMessage = onSendMessage,
|
onSendMessage = onSendMessage,
|
||||||
|
showBottomSheet = showChat,
|
||||||
|
onShowBottomSheetChanged = onShowChatChanged,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(200.dp)
|
.height(200.dp)
|
||||||
@@ -225,10 +264,15 @@ private fun PortraitContent(
|
|||||||
|
|
||||||
PlayersAndSettings(
|
PlayersAndSettings(
|
||||||
game = game,
|
game = game,
|
||||||
|
gameFiles = gameFiles,
|
||||||
|
onGameSelected = onGameSelected,
|
||||||
|
showGamePicker = showGamePicker,
|
||||||
|
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||||
players = players,
|
players = players,
|
||||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||||
maxBuffer = maxBuffer,
|
maxBuffer = maxBuffer,
|
||||||
onMaxBufferChanged = onMaxBufferChanged,
|
onMaxBufferChanged = onMaxBufferChanged,
|
||||||
|
isHosting = isHosting,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = DolphinTheme.scaffoldPadding),
|
.padding(horizontal = DolphinTheme.scaffoldPadding),
|
||||||
)
|
)
|
||||||
@@ -244,7 +288,13 @@ private fun LandscapeContent(
|
|||||||
isHosting: Boolean,
|
isHosting: Boolean,
|
||||||
messages: List<NetplayMessage>,
|
messages: List<NetplayMessage>,
|
||||||
onSendMessage: (String) -> Unit,
|
onSendMessage: (String) -> Unit,
|
||||||
|
showChat: Boolean,
|
||||||
|
onShowChatChanged: (Boolean) -> Unit,
|
||||||
game: String,
|
game: String,
|
||||||
|
gameFiles: List<GameFile>,
|
||||||
|
onGameSelected: (GameFile) -> Unit,
|
||||||
|
showGamePicker: Boolean,
|
||||||
|
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||||
players: List<Player>,
|
players: List<Player>,
|
||||||
hostInputAuthorityEnabled: Boolean,
|
hostInputAuthorityEnabled: Boolean,
|
||||||
maxBuffer: Int,
|
maxBuffer: Int,
|
||||||
@@ -257,6 +307,8 @@ private fun LandscapeContent(
|
|||||||
Chat(
|
Chat(
|
||||||
messages = messages,
|
messages = messages,
|
||||||
onSendMessage = onSendMessage,
|
onSendMessage = onSendMessage,
|
||||||
|
showBottomSheet = showChat,
|
||||||
|
onShowBottomSheetChanged = onShowChatChanged,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
@@ -270,10 +322,15 @@ private fun LandscapeContent(
|
|||||||
) {
|
) {
|
||||||
PlayersAndSettings(
|
PlayersAndSettings(
|
||||||
game = game,
|
game = game,
|
||||||
|
gameFiles = gameFiles,
|
||||||
|
onGameSelected = onGameSelected,
|
||||||
|
showGamePicker = showGamePicker,
|
||||||
|
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||||
players = players,
|
players = players,
|
||||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||||
maxBuffer = maxBuffer,
|
maxBuffer = maxBuffer,
|
||||||
onMaxBufferChanged = onMaxBufferChanged,
|
onMaxBufferChanged = onMaxBufferChanged,
|
||||||
|
isHosting = isHosting,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = DolphinTheme.scaffoldPadding)
|
.padding(horizontal = DolphinTheme.scaffoldPadding)
|
||||||
)
|
)
|
||||||
@@ -288,22 +345,27 @@ private fun LandscapeContent(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PlayersAndSettings(
|
private fun PlayersAndSettings(
|
||||||
game: String,
|
game: String,
|
||||||
|
gameFiles: List<GameFile>,
|
||||||
|
onGameSelected: (GameFile) -> Unit,
|
||||||
|
showGamePicker: Boolean,
|
||||||
|
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||||
players: List<Player>,
|
players: List<Player>,
|
||||||
hostInputAuthorityEnabled: Boolean,
|
hostInputAuthorityEnabled: Boolean,
|
||||||
maxBuffer: Int,
|
maxBuffer: Int,
|
||||||
onMaxBufferChanged: (Int) -> Unit,
|
onMaxBufferChanged: (Int) -> Unit,
|
||||||
|
isHosting: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
GamePicker(
|
||||||
value = game,
|
game = game,
|
||||||
onValueChange = {},
|
gameFiles = gameFiles,
|
||||||
label = { Text(stringResource(R.string.netplay_game_label)) },
|
onGameSelected = onGameSelected,
|
||||||
readOnly = true,
|
showGamePicker = showGamePicker,
|
||||||
modifier = Modifier
|
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||||
.fillMaxWidth()
|
isHosting = isHosting,
|
||||||
)
|
)
|
||||||
|
|
||||||
MenuSpacer()
|
MenuSpacer()
|
||||||
@@ -345,6 +407,8 @@ private fun PlayersAndSettings(
|
|||||||
private fun Chat(
|
private fun Chat(
|
||||||
messages: List<NetplayMessage>,
|
messages: List<NetplayMessage>,
|
||||||
onSendMessage: (String) -> Unit,
|
onSendMessage: (String) -> Unit,
|
||||||
|
showBottomSheet: Boolean,
|
||||||
|
onShowBottomSheetChanged: (Boolean) -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -361,12 +425,18 @@ private fun Chat(
|
|||||||
draftMessage = ""
|
draftMessage = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
val density = LocalDensity.current
|
||||||
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val bottomSheetState = remember {
|
||||||
|
SheetState(
|
||||||
|
skipPartiallyExpanded = true,
|
||||||
|
density = density,
|
||||||
|
initialValue = if (showBottomSheet) SheetValue.Expanded else SheetValue.Hidden,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { showBottomSheet = false },
|
onDismissRequest = { onShowBottomSheetChanged(false) },
|
||||||
sheetState = bottomSheetState,
|
sheetState = bottomSheetState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
@@ -407,7 +477,7 @@ private fun Chat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
OutlinedBox(
|
OutlinedBox(
|
||||||
onClick = { showBottomSheet = true },
|
onClick = { onShowBottomSheetChanged(true) },
|
||||||
label = { Text(stringResource(R.string.netplay_chat_label)) },
|
label = { Text(stringResource(R.string.netplay_chat_label)) },
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
@@ -422,6 +492,123 @@ private fun Chat(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun GamePicker(
|
||||||
|
game: String,
|
||||||
|
gameFiles: List<GameFile>,
|
||||||
|
onGameSelected: (GameFile) -> Unit,
|
||||||
|
showGamePicker: Boolean,
|
||||||
|
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||||
|
isHosting: Boolean,
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val bottomSheetState = remember {
|
||||||
|
SheetState(
|
||||||
|
skipPartiallyExpanded = true,
|
||||||
|
density = density,
|
||||||
|
initialValue = if (showGamePicker) SheetValue.Expanded else SheetValue.Hidden,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showGamePicker) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { onShowGamePickerChanged(false) },
|
||||||
|
sheetState = bottomSheetState,
|
||||||
|
modifier = Modifier.statusBarsPadding()
|
||||||
|
) {
|
||||||
|
GameList(
|
||||||
|
gameFiles = gameFiles,
|
||||||
|
onGameSelected = { gameFile ->
|
||||||
|
onGameSelected(gameFile)
|
||||||
|
onShowGamePickerChanged(false)
|
||||||
|
},
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
start = DolphinTheme.scaffoldPadding,
|
||||||
|
end = DolphinTheme.scaffoldPadding,
|
||||||
|
bottom = 16.dp
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = game,
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text(stringResource(R.string.netplay_game_label)) },
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
if (isHosting) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
|
.clickable { onShowGamePickerChanged(true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GameList(
|
||||||
|
gameFiles: List<GameFile>,
|
||||||
|
onGameSelected: (GameFile) -> Unit,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(),
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
items(gameFiles, key = { it.getPath() }) { gameFile ->
|
||||||
|
GameGridItem(
|
||||||
|
gameFile = gameFile,
|
||||||
|
onClick = { onGameSelected(gameFile) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GameGridItem(
|
||||||
|
gameFile: GameFile,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(gameFile)
|
||||||
|
.error(R.drawable.no_banner)
|
||||||
|
.build(),
|
||||||
|
contentDescription = gameFile.getTitle(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
imageLoader = CoilUtils.imageLoader,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(0.7f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = gameFile.getTitle(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 2,
|
||||||
|
minLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A table arranged into columns sized to wrap the largest item. Except the
|
* A table arranged into columns sized to wrap the largest item. Except the
|
||||||
* first column which takes up the remaining space left by the other columns.
|
* first column which takes up the remaining space left by the other columns.
|
||||||
@@ -787,6 +974,8 @@ private fun PreviewNetplayScreen() {
|
|||||||
game = "Game name",
|
game = "Game name",
|
||||||
isHosting = true,
|
isHosting = true,
|
||||||
onStartGame = {},
|
onStartGame = {},
|
||||||
|
onGameSelected = {},
|
||||||
|
gameFiles = emptyList(),
|
||||||
hostInputAuthorityEnabled = true,
|
hostInputAuthorityEnabled = true,
|
||||||
maxBuffer = 10,
|
maxBuffer = 10,
|
||||||
onMaxBufferChanged = {},
|
onMaxBufferChanged = {},
|
||||||
|
|||||||
+2
-1
@@ -115,7 +115,8 @@ enum class StringSetting(
|
|||||||
""
|
""
|
||||||
),
|
),
|
||||||
NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"),
|
NETPLAY_ADDRESS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Address", "127.0.0.1"),
|
||||||
NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player");
|
NETPLAY_NICKNAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Nickname", "Player"),
|
||||||
|
NETPLAY_GAME(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "Game", "");
|
||||||
|
|
||||||
override val isOverridden: Boolean
|
override val isOverridden: Boolean
|
||||||
get() = NativeConfig.isOverridden(file, section, key)
|
get() = NativeConfig.isOverridden(file, section, key)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class GameCoverKeyer : Keyer<GameFile> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object CoilUtils {
|
object CoilUtils {
|
||||||
private val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext())
|
val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext())
|
||||||
.components {
|
.components {
|
||||||
add(GameCoverKeyer())
|
add(GameCoverKeyer())
|
||||||
add(GameCoverFetcher.Factory())
|
add(GameCoverFetcher.Factory())
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ androidx-cardview = { group = "androidx.cardview", name = "cardview", version.re
|
|||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||||
androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
|
androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
|
||||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
@@ -45,6 +46,7 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
|
|||||||
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" }
|
||||||
androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" }
|
androidx-leanback = { group = "androidx.leanback", name = "leanback", version.ref = "leanback" }
|
||||||
|
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||||
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
|
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
|
||||||
@@ -55,6 +57,7 @@ androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "s
|
|||||||
androidx-tvprovider = { group = "androidx.tvprovider", name = "tvprovider", version.ref = "tvprovider" }
|
androidx-tvprovider = { group = "androidx.tvprovider", name = "tvprovider", version.ref = "tvprovider" }
|
||||||
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
|
||||||
coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
|
coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
|
||||||
|
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||||
desugar_jdk_libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
desugar_jdk_libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||||
filepicker = { group = "com.nononsenseapps", name = "filepicker", version.ref = "filepicker" }
|
filepicker = { group = "com.nononsenseapps", name = "filepicker", version.ref = "filepicker" }
|
||||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||||
|
|||||||
@@ -148,6 +148,21 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv
|
|||||||
return reinterpret_cast<jlong>(server);
|
return reinterpret_cast<jlong>(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeChangeGame(JNIEnv* env,
|
||||||
|
jobject obj,
|
||||||
|
jobject jgame_file)
|
||||||
|
{
|
||||||
|
auto* server = GetServerPointer(env, obj);
|
||||||
|
if (!server)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& game_file = *reinterpret_cast<std::shared_ptr<const UICommon::GameFile>*>(
|
||||||
|
env->GetLongField(jgame_file, IDCache::GetGameFilePointer()));
|
||||||
|
|
||||||
|
server->ChangeGame(game_file->GetSyncIdentifier(), game_file->GetLongName());
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env,
|
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env,
|
||||||
jobject obj)
|
jobject obj)
|
||||||
|
|||||||
Reference in new issue
Block a user