mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2025-07-24 06:45:46 +00:00
Added the public lobby to android. (#125)
This is adapted from kleidis old PR to Azahar. Changes from it: - Fixed inconsistent button styling in the dialog for connection - Allowed to hide both empty and full rooms. - Proper serving of preferred games - Enables web service for android by default - Better implementation of multiplayer.cpp that works with oop Also fixes the room network class and turns it into a static namespace in network Signed-off-by: Aleksandr Popovich <alekpopo@pm.me> Co-authored-by: swurl <swurl@swurl.xyz> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/125 Co-authored-by: Aleksandr Popovich <alekpopo@pm.me> Co-committed-by: Aleksandr Popovich <alekpopo@pm.me>
This commit is contained in:
parent
7e13da47af
commit
76fa525592
99 changed files with 1470 additions and 498 deletions
|
@ -1,4 +1,7 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
@ -162,7 +165,7 @@ android {
|
|||
arguments(
|
||||
"-DENABLE_QT=0", // Don't use QT
|
||||
"-DENABLE_SDL2=0", // Don't use SDL
|
||||
"-DENABLE_WEB_SERVICE=0", // Don't use telemetry
|
||||
"-DENABLE_WEB_SERVICE=1", // Enable web service
|
||||
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
|
||||
"-DYUZU_USE_BUNDLED_VCPKG=ON",
|
||||
"-DYUZU_USE_BUNDLED_FFMPEG=ON",
|
||||
|
@ -176,9 +179,9 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
tasks.create<Delete>("ktlintReset") {
|
||||
delete(File(buildDir.path + File.separator + "intermediates/ktLint"))
|
||||
}
|
||||
tasks.register<Delete>("ktlintReset", fun Delete.() {
|
||||
delete(File(layout.buildDirectory.toString() + File.separator + "intermediates/ktLint"))
|
||||
})
|
||||
|
||||
val showFormatHelp = {
|
||||
logger.lifecycle(
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<!--
|
||||
SPDX-FileCopyrightText: Eden Emulator Project
|
||||
|
@ -15,6 +18,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -270,8 +270,7 @@ object NativeLibrary {
|
|||
NetPlayManager.clearChat()
|
||||
}
|
||||
|
||||
|
||||
external fun netPlayInit()
|
||||
external fun initMultiplayer()
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.dialogs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import info.debatty.java.stringsimilarity.Jaccard
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.DialogLobbyBrowserBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ItemLobbyRoomBinding
|
||||
import org.yuzu.yuzu_emu.network.NetPlayManager
|
||||
import java.util.Locale
|
||||
|
||||
class LobbyBrowser(context: Context) : BottomSheetDialog(context) {
|
||||
private lateinit var binding: DialogLobbyBrowserBinding
|
||||
private lateinit var adapter: LobbyRoomAdapter
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.skipCollapsed =
|
||||
context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
binding = DialogLobbyBrowserBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.emptyRefreshButton.setOnClickListener {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
refreshRoomList()
|
||||
}
|
||||
|
||||
setupRecyclerView()
|
||||
setupRefreshButton()
|
||||
refreshRoomList()
|
||||
setupSearchBar()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
adapter = LobbyRoomAdapter { room -> handleRoomSelection(room) }
|
||||
|
||||
binding.roomList.apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = this@LobbyBrowser.adapter
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRefreshButton() {
|
||||
binding.refreshButton.setOnClickListener {
|
||||
binding.refreshButton.isEnabled = false
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
refreshRoomList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSearchBar() {
|
||||
binding.chipHideFull.setOnCheckedChangeListener { _, _ -> adapter.filterAndSearch() }
|
||||
binding.chipHideEmpty.setOnCheckedChangeListener { _, _ -> adapter.filterAndSearch() }
|
||||
|
||||
binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
|
||||
if (text.toString().isNotEmpty()) {
|
||||
binding.clearButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.clearButton.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchText.setOnEditorActionListener { v, action, _ ->
|
||||
if (action == EditorInfo.IME_ACTION_DONE) {
|
||||
v.clearFocus()
|
||||
|
||||
val imm = context.getSystemService<InputMethodManager>()
|
||||
imm?.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
|
||||
adapter.filterAndSearch()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnSubmit.setOnClickListener { adapter.filterAndSearch() }
|
||||
|
||||
binding.clearButton.setOnClickListener {
|
||||
binding.searchText.setText("")
|
||||
adapter.updateRooms(NetPlayManager.getPublicRooms())
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshRoomList() {
|
||||
NetPlayManager.refreshRoomListAsync { rooms ->
|
||||
binding.emptyView.visibility = if (rooms.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.roomList.visibility = if (rooms.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.appbar.visibility = if (rooms.isEmpty()) View.GONE else View.VISIBLE
|
||||
adapter.updateRooms(rooms)
|
||||
adapter.filterAndSearch()
|
||||
binding.refreshButton.isEnabled = true
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRoomSelection(room: NetPlayManager.RoomInfo) {
|
||||
if (room.hasPassword) {
|
||||
showPasswordDialog(room)
|
||||
} else {
|
||||
joinRoom(room, "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPasswordDialog(room: NetPlayManager.RoomInfo) {
|
||||
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_password_input, null)
|
||||
val passwordInput = dialogView.findViewById<TextInputEditText>(R.id.password_input)
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(context.getString(R.string.multiplayer_password_required))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.multiplayer_join_room) { _, _ ->
|
||||
joinRoom(room, passwordInput.text.toString())
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun joinRoom(room: NetPlayManager.RoomInfo, password: String) {
|
||||
val username = NetPlayManager.getUsername(context)
|
||||
|
||||
Thread {
|
||||
val result = NetPlayManager.netPlayJoinRoom(room.ip, room.port, username, password)
|
||||
|
||||
handler.post {
|
||||
if (result == 0) {
|
||||
dismiss()
|
||||
NetPlayDialog(context).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
inner class LobbyRoomAdapter(private val onRoomSelected: (NetPlayManager.RoomInfo) -> Unit) :
|
||||
RecyclerView.Adapter<LobbyRoomAdapter.RoomViewHolder>() {
|
||||
|
||||
private val rooms = mutableListOf<NetPlayManager.RoomInfo>()
|
||||
|
||||
inner class RoomViewHolder(private val binding: ItemLobbyRoomBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(room: NetPlayManager.RoomInfo) {
|
||||
binding.roomName.text = room.name
|
||||
binding.roomOwner.text = room.owner
|
||||
binding.playerCount.text = context.getString(
|
||||
R.string.multiplayer_player_count,
|
||||
room.members.size,
|
||||
room.maxPlayers
|
||||
)
|
||||
|
||||
binding.lockIcon.visibility = if (room.hasPassword) View.VISIBLE else View.GONE
|
||||
|
||||
if (room.preferredGameName.isNotEmpty() && room.preferredGameId != 0L) {
|
||||
binding.gameName.text = room.preferredGameName
|
||||
} else {
|
||||
binding.gameName.text = context.getString(R.string.multiplayer_no_game_info)
|
||||
}
|
||||
|
||||
itemView.setOnClickListener { onRoomSelected(room) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomViewHolder {
|
||||
val binding = ItemLobbyRoomBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return RoomViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RoomViewHolder, position: Int) {
|
||||
holder.bind(rooms[position])
|
||||
}
|
||||
|
||||
override fun getItemCount() = rooms.size
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateRooms(newRooms: List<NetPlayManager.RoomInfo>) {
|
||||
rooms.clear()
|
||||
rooms.addAll(newRooms)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun filterAndSearch() {
|
||||
if (binding.searchText.text.toString().isEmpty() &&
|
||||
!binding.chipHideFull.isChecked && !binding.chipHideEmpty.isChecked
|
||||
) {
|
||||
adapter.updateRooms(NetPlayManager.getPublicRooms())
|
||||
return
|
||||
}
|
||||
|
||||
val baseList = NetPlayManager.getPublicRooms()
|
||||
val filteredList = baseList.filter { room ->
|
||||
(!binding.chipHideFull.isChecked || room.members.size < room.maxPlayers) &&
|
||||
(!binding.chipHideEmpty.isChecked || room.members.isNotEmpty())
|
||||
}
|
||||
|
||||
if (binding.searchText.text.toString().isEmpty() &&
|
||||
(binding.chipHideFull.isChecked || binding.chipHideEmpty.isChecked)
|
||||
) {
|
||||
adapter.updateRooms(filteredList)
|
||||
return
|
||||
}
|
||||
|
||||
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
||||
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
|
||||
val sortedList: List<NetPlayManager.RoomInfo> = filteredList.mapNotNull { room ->
|
||||
val roomName = room.name.lowercase(Locale.getDefault())
|
||||
|
||||
val score = searchAlgorithm.similarity(roomName, searchTerm)
|
||||
if (score > 0.03) {
|
||||
ScoreItem(score, room)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.sortedByDescending { it ->
|
||||
it.score
|
||||
}.map { it.item }
|
||||
adapter.updateRooms(sortedList)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ScoreItem(val score: Double, val item: NetPlayManager.RoomInfo)
|
||||
}
|
|
@ -1,12 +1,9 @@
|
|||
// Copyright 2024 Mandarine Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.dialogs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import android.content.res.Configuration
|
||||
|
@ -16,6 +13,7 @@ import android.os.Looper
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
@ -32,10 +30,13 @@ import org.yuzu.yuzu_emu.databinding.ItemButtonNetplayBinding
|
|||
import org.yuzu.yuzu_emu.databinding.ItemTextNetplayBinding
|
||||
import org.yuzu.yuzu_emu.utils.CompatUtils
|
||||
import org.yuzu.yuzu_emu.network.NetPlayManager
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
|
||||
class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
||||
private lateinit var adapter: NetPlayAdapter
|
||||
|
||||
private val gameNameList: MutableList<Array<String>> = mutableListOf()
|
||||
private val gameIdList: MutableList<Array<Long>> = mutableListOf()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -71,6 +72,17 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
else -> {
|
||||
DialogMultiplayerConnectBinding.inflate(layoutInflater).apply {
|
||||
setContentView(root)
|
||||
for (game in GameHelper.cachedGameList) {
|
||||
val gameName = game.title
|
||||
if (gameNameList.none { it[0] == gameName }) {
|
||||
gameNameList.add(arrayOf(gameName))
|
||||
}
|
||||
|
||||
val gameId = game.programId.toLong()
|
||||
if (gameIdList.none { it[0] == gameId }) {
|
||||
gameIdList.add(arrayOf(gameId))
|
||||
}
|
||||
}
|
||||
btnCreate.setOnClickListener {
|
||||
showNetPlayInputDialog(true)
|
||||
dismiss()
|
||||
|
@ -79,6 +91,10 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
showNetPlayInputDialog(false)
|
||||
dismiss()
|
||||
}
|
||||
btnLobbyBrowser.setOnClickListener {
|
||||
LobbyBrowser(context).show()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -208,10 +224,11 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
override fun getItemCount() = netPlayItems.size
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun refreshAdapterItems() {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
NetPlayManager.setOnAdapterRefreshListener() { type, msg ->
|
||||
NetPlayManager.setOnAdapterRefreshListener() { _, _ ->
|
||||
handler.post {
|
||||
adapter.netPlayItems.clear()
|
||||
adapter.loadMultiplayerMenu()
|
||||
|
@ -244,6 +261,17 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
binding.ipPort.setText(NetPlayManager.getRoomPort(activity))
|
||||
binding.username.setText(NetPlayManager.getUsername(activity))
|
||||
|
||||
binding.dropdownPreferredGameName.apply {
|
||||
setAdapter(
|
||||
ArrayAdapter(
|
||||
activity,
|
||||
R.layout.dropdown_item,
|
||||
gameNameList.map { it[0] }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
binding.preferredGameName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
|
||||
binding.roomName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
|
||||
binding.maxPlayersContainer.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
|
||||
binding.maxPlayersLabel.text = context.getString(R.string.multiplayer_max_players_value, binding.maxPlayers.value.toInt())
|
||||
|
@ -259,6 +287,8 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
val ipAddress = binding.ipAddress.text.toString()
|
||||
val username = binding.username.text.toString()
|
||||
val portStr = binding.ipPort.text.toString()
|
||||
val preferredGameName = binding.dropdownPreferredGameName.text.toString()
|
||||
val preferredGameId = gameIdList[gameNameList.indexOfFirst { it[0] == preferredGameName }][0]
|
||||
val password = binding.password.text.toString()
|
||||
val port = portStr.toIntOrNull() ?: run {
|
||||
Toast.makeText(activity, R.string.multiplayer_port_invalid, Toast.LENGTH_LONG).show()
|
||||
|
@ -276,6 +306,13 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (isCreateRoom && preferredGameName.isEmpty()) {
|
||||
Toast.makeText(activity, R.string.multiplayer_preferred_game_name_invalid, Toast.LENGTH_LONG).show();
|
||||
binding.btnConfirm.isEnabled = false
|
||||
binding.btnConfirm.text = activity.getString(R.string.original_button_text)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (ipAddress.length < 7 || username.length < 5) {
|
||||
Toast.makeText(activity, R.string.multiplayer_input_invalid, Toast.LENGTH_LONG).show()
|
||||
binding.btnConfirm.isEnabled = true
|
||||
|
@ -283,7 +320,7 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
} else {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
val result = if (isCreateRoom) {
|
||||
NetPlayManager.netPlayCreateRoom(ipAddress, port, username, password, roomName, maxPlayers)
|
||||
NetPlayManager.netPlayCreateRoom(ipAddress, port, username, preferredGameName, preferredGameId, password, roomName, maxPlayers)
|
||||
} else {
|
||||
NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password)
|
||||
}
|
||||
|
@ -397,4 +434,4 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
|
|||
|
||||
enum class StringSetting(override val key: String) : AbstractStringSetting {
|
||||
DRIVER_PATH("driver_path"),
|
||||
DEVICE_NAME("device_name");
|
||||
DEVICE_NAME("device_name"),
|
||||
WEB_TOKEN("yuzu_token"),
|
||||
;
|
||||
|
||||
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
|
||||
|
||||
|
|
|
@ -274,6 +274,13 @@ abstract class SettingsItem(
|
|||
descriptionId = R.string.use_custom_rtc_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
StringInputSetting(
|
||||
StringSetting.WEB_TOKEN,
|
||||
titleId = R.string.web_token,
|
||||
descriptionId = R.string.web_token_description
|
||||
)
|
||||
)
|
||||
put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.ui
|
||||
|
@ -200,6 +200,10 @@ class SettingsFragmentPresenter(
|
|||
add(IntSetting.LANGUAGE_INDEX.key)
|
||||
add(BooleanSetting.USE_CUSTOM_RTC.key)
|
||||
add(LongSetting.CUSTOM_RTC.key)
|
||||
|
||||
// TODO(alekpop): Add functionality
|
||||
// add(HeaderSetting(R.string.network))
|
||||
// add(StringSetting.WEB_TOKEN.key)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.Manifest
|
||||
|
@ -128,7 +124,7 @@ class HomeSettingsFragment : Fragment() {
|
|||
R.string.multiplayer_description,
|
||||
R.drawable.ic_two_users,
|
||||
{
|
||||
val action = mainActivity.displayMultiplayerDialog()
|
||||
mainActivity.displayMultiplayerDialog()
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
// Copyright 2024 Mandarine Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
package org.yuzu.yuzu_emu.network
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.format.Formatter
|
||||
|
@ -19,10 +17,27 @@ import androidx.preference.PreferenceManager
|
|||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.dialogs.ChatMessage
|
||||
import java.net.Inet4Address
|
||||
|
||||
object NetPlayManager {
|
||||
external fun netPlayCreateRoom(ipAddress: String, port: Int, username: String, password: String, roomName: String, maxPlayers: Int): Int
|
||||
external fun netPlayJoinRoom(ipAddress: String, port: Int, username: String, password: String): Int
|
||||
external fun netPlayCreateRoom(
|
||||
ipAddress: String,
|
||||
port: Int,
|
||||
username: String,
|
||||
preferredGameName: String,
|
||||
preferredGameId: Long,
|
||||
password: String,
|
||||
roomName: String,
|
||||
maxPlayers: Int
|
||||
): Int
|
||||
|
||||
external fun netPlayJoinRoom(
|
||||
ipAddress: String,
|
||||
port: Int,
|
||||
username: String,
|
||||
password: String
|
||||
): Int
|
||||
|
||||
external fun netPlayRoomInfo(): Array<String>
|
||||
external fun netPlayIsJoined(): Boolean
|
||||
external fun netPlayIsHostedRoom(): Boolean
|
||||
|
@ -33,6 +48,28 @@ object NetPlayManager {
|
|||
external fun netPlayGetBanList(): Array<String>
|
||||
external fun netPlayBanUser(username: String)
|
||||
external fun netPlayUnbanUser(username: String)
|
||||
external fun netPlayGetPublicRooms(): Array<String>
|
||||
|
||||
data class RoomInfo(
|
||||
val name: String,
|
||||
val hasPassword: Boolean,
|
||||
val maxPlayers: Int,
|
||||
val ip: String,
|
||||
val port: Int,
|
||||
val description: String,
|
||||
val owner: String,
|
||||
val preferredGameId: Long,
|
||||
val preferredGameName: String,
|
||||
val members: MutableList<RoomMember> = mutableListOf()
|
||||
)
|
||||
|
||||
data class RoomMember(
|
||||
val username: String,
|
||||
val nickname: String,
|
||||
val gameId: Long,
|
||||
val gameName: String
|
||||
)
|
||||
|
||||
|
||||
private var messageListener: ((Int, String) -> Unit)? = null
|
||||
private var adapterRefreshListener: ((Int, String) -> Unit)? = null
|
||||
|
@ -41,11 +78,57 @@ object NetPlayManager {
|
|||
messageListener = listener
|
||||
}
|
||||
|
||||
fun getPublicRooms(): List<RoomInfo> {
|
||||
val roomData = netPlayGetPublicRooms()
|
||||
val rooms = mutableMapOf<String, RoomInfo>()
|
||||
|
||||
for (data in roomData) {
|
||||
val parts = data.split("|")
|
||||
|
||||
if (parts[0] == "MEMBER" && parts.size >= 6) {
|
||||
val roomName = parts[1]
|
||||
val member = RoomMember(
|
||||
username = parts[2],
|
||||
nickname = parts[3],
|
||||
gameId = parts[4].toLongOrNull() ?: 0L,
|
||||
gameName = parts[5]
|
||||
)
|
||||
rooms[roomName]?.members?.add(member)
|
||||
} else if (parts.size >= 9) {
|
||||
val roomInfo = RoomInfo(
|
||||
name = parts[0],
|
||||
hasPassword = parts[1] == "1",
|
||||
maxPlayers = parts[2].toIntOrNull() ?: 0,
|
||||
ip = parts[3],
|
||||
port = parts[4].toIntOrNull() ?: 0,
|
||||
description = parts[5],
|
||||
owner = parts[6],
|
||||
preferredGameId = parts[7].toLongOrNull() ?: 0L,
|
||||
preferredGameName = parts[8]
|
||||
)
|
||||
rooms[roomInfo.name] = roomInfo
|
||||
}
|
||||
}
|
||||
|
||||
return rooms.values.toList()
|
||||
}
|
||||
|
||||
fun refreshRoomListAsync(callback: (List<RoomInfo>) -> Unit) {
|
||||
Thread {
|
||||
val rooms = getPublicRooms()
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
callback(rooms)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun setOnAdapterRefreshListener(listener: (Int, String) -> Unit) {
|
||||
adapterRefreshListener = listener
|
||||
}
|
||||
|
||||
fun getUsername(activity: Context): String { val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
fun getUsername(activity: Context): String {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
val name = "Eden${(Math.random() * 100).toInt()}"
|
||||
return prefs.getString("NetPlayUsername", name) ?: name
|
||||
}
|
||||
|
@ -103,31 +186,36 @@ object NetPlayManager {
|
|||
if (parts.size == 2) {
|
||||
val nickname = parts[0].trim()
|
||||
val chatMessage = parts[1].trim()
|
||||
addChatMessage(ChatMessage(
|
||||
nickname = nickname,
|
||||
username = "",
|
||||
message = chatMessage
|
||||
))
|
||||
addChatMessage(
|
||||
ChatMessage(
|
||||
nickname = nickname,
|
||||
username = "",
|
||||
message = chatMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NetPlayStatus.MEMBER_JOIN,
|
||||
NetPlayStatus.MEMBER_LEAVE,
|
||||
NetPlayStatus.MEMBER_KICKED,
|
||||
NetPlayStatus.MEMBER_BANNED -> {
|
||||
addChatMessage(ChatMessage(
|
||||
nickname = "System",
|
||||
username = "",
|
||||
message = message
|
||||
))
|
||||
addChatMessage(
|
||||
ChatMessage(
|
||||
nickname = "System",
|
||||
username = "",
|
||||
message = message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (!isChatOpen) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (!isChatOpen) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
messageListener?.invoke(type, msg)
|
||||
|
@ -159,34 +247,59 @@ object NetPlayManager {
|
|||
NetPlayStatus.ROOM_MODERATOR -> context.getString(R.string.multiplayer_room_moderator)
|
||||
NetPlayStatus.MEMBER_JOIN -> context.getString(R.string.multiplayer_member_join, msg)
|
||||
NetPlayStatus.MEMBER_LEAVE -> context.getString(R.string.multiplayer_member_leave, msg)
|
||||
NetPlayStatus.MEMBER_KICKED -> context.getString(R.string.multiplayer_member_kicked, msg)
|
||||
NetPlayStatus.MEMBER_BANNED -> context.getString(R.string.multiplayer_member_banned, msg)
|
||||
NetPlayStatus.MEMBER_KICKED -> context.getString(
|
||||
R.string.multiplayer_member_kicked,
|
||||
msg
|
||||
)
|
||||
|
||||
NetPlayStatus.MEMBER_BANNED -> context.getString(
|
||||
R.string.multiplayer_member_banned,
|
||||
msg
|
||||
)
|
||||
|
||||
NetPlayStatus.ADDRESS_UNBANNED -> context.getString(R.string.multiplayer_address_unbanned)
|
||||
NetPlayStatus.CHAT_MESSAGE -> msg
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
fun getIpAddressByWifi(activity: Activity): String {
|
||||
var ipAddress = 0
|
||||
val wifiManager = activity.getSystemService(WifiManager::class.java)
|
||||
val wifiInfo = wifiManager.connectionInfo
|
||||
if (wifiInfo != null) {
|
||||
ipAddress = wifiInfo.ipAddress
|
||||
}
|
||||
fun isConnectedToWifi(activity: Activity): Boolean {
|
||||
val connectivityManager = activity.getSystemService(ConnectivityManager::class.java)
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
}
|
||||
|
||||
if (ipAddress == 0) {
|
||||
val dhcpInfo = wifiManager.dhcpInfo
|
||||
if (dhcpInfo != null) {
|
||||
ipAddress = dhcpInfo.ipAddress
|
||||
fun getIpAddressByWifi(activity: Activity): String {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// For Android 12 (API 31) and above
|
||||
val connectivityManager = activity.getSystemService(ConnectivityManager::class.java)
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
|
||||
if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
|
||||
val linkProperties = connectivityManager.getLinkProperties(network)
|
||||
linkProperties?.linkAddresses?.firstOrNull { it.address is Inet4Address }?.let {
|
||||
return it.address.hostAddress ?: "192.168.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (ipAddress == 0) {
|
||||
"192.168.0.1"
|
||||
} else {
|
||||
Formatter.formatIpAddress(ipAddress)
|
||||
// For Android 11 (API 30) and below
|
||||
try {
|
||||
val connectivityManager = activity.getSystemService(ConnectivityManager::class.java)
|
||||
val network = connectivityManager.activeNetwork
|
||||
if (network != null) {
|
||||
val linkProperties = connectivityManager.getLinkProperties(network)
|
||||
linkProperties?.linkAddresses?.firstOrNull { it.address is Inet4Address }?.let {
|
||||
return it.address.hostAddress ?: "192.168.0.1"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return "192.168.0.1"
|
||||
}
|
||||
|
||||
fun getBanList(): List<String> {
|
||||
|
@ -223,4 +336,4 @@ object NetPlayManager {
|
|||
const val ADDRESS_UNBANNED = 26
|
||||
const val CHAT_MESSAGE = 27
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.ui
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
package org.yuzu.yuzu_emu.ui.main
|
||||
|
||||
import android.content.Intent
|
||||
|
@ -72,9 +68,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
|
||||
ThemeHelper.ThemeChangeListener(this)
|
||||
ThemeHelper.setTheme(this)
|
||||
NativeLibrary.netPlayInit()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
NativeLibrary.initMultiplayer()
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
// Copyright 2024 Mandarine Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
@ -13,11 +16,15 @@ import org.yuzu.yuzu_emu.YuzuApplication
|
|||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
|
||||
object GameHelper {
|
||||
private const val KEY_OLD_GAME_PATH = "game_path"
|
||||
const val KEY_GAMES = "Games"
|
||||
|
||||
var cachedGameList = mutableListOf<Game>()
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
fun getGames(): List<Game> {
|
||||
|
@ -29,7 +36,7 @@ object GameHelper {
|
|||
val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
|
||||
if (oldGamesDir.isNotEmpty()) {
|
||||
gameDirs.add(GameDir(oldGamesDir, true))
|
||||
preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
|
||||
preferences.edit() { remove(KEY_OLD_GAME_PATH) }
|
||||
}
|
||||
gameDirs.addAll(NativeConfig.getGameDirs())
|
||||
|
||||
|
@ -44,7 +51,7 @@ object GameHelper {
|
|||
|
||||
val badDirs = mutableListOf<Int>()
|
||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||
val gameDirUri = Uri.parse(gameDir.uriString)
|
||||
val gameDirUri = gameDir.uriString.toUri()
|
||||
val isValid = FileUtil.isTreeUriValid(gameDirUri)
|
||||
if (isValid) {
|
||||
addGamesRecursive(
|
||||
|
@ -72,11 +79,12 @@ object GameHelper {
|
|||
games.forEach {
|
||||
serializedGames.add(Json.encodeToString(it))
|
||||
}
|
||||
preferences.edit()
|
||||
.remove(KEY_GAMES)
|
||||
.putStringSet(KEY_GAMES, serializedGames)
|
||||
.apply()
|
||||
preferences.edit() {
|
||||
remove(KEY_GAMES)
|
||||
.putStringSet(KEY_GAMES, serializedGames)
|
||||
}
|
||||
|
||||
cachedGameList = games.toMutableList()
|
||||
return games.toList()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -68,12 +68,17 @@
|
|||
#include "video_core/vulkan_common/vulkan_instance.h"
|
||||
#include "video_core/vulkan_common/vulkan_surface.h"
|
||||
#include "video_core/shader_notify.h"
|
||||
#include "network/announce_multiplayer_session.h"
|
||||
|
||||
#define jconst [[maybe_unused]] const auto
|
||||
#define jauto [[maybe_unused]] auto
|
||||
|
||||
static EmulationSession s_instance;
|
||||
|
||||
//Abdroid Multiplayer which can be initialized with parameters
|
||||
std::unique_ptr<AndroidMultiplayer> multiplayer{nullptr};
|
||||
std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
|
||||
|
||||
EmulationSession::EmulationSession() {
|
||||
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
|
||||
}
|
||||
|
@ -916,12 +921,33 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobje
|
|||
return ContentManager::AreKeysPresent();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_NativeLibrary_initMultiplayer(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
if (multiplayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>();
|
||||
|
||||
multiplayer = std::make_unique<AndroidMultiplayer>(s_instance.System(), announce_multiplayer_session);
|
||||
multiplayer->NetworkInit();
|
||||
}
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetPublicRooms(
|
||||
JNIEnv *env, [[maybe_unused]] jobject obj) {
|
||||
return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetPublicRooms());
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayCreateRoom(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
|
||||
jstring username, jstring password, jstring room_name, jint max_players) {
|
||||
jstring username, jstring preferredGameName, jlong preferredGameId, jstring password,
|
||||
jstring room_name, jint max_players) {
|
||||
return static_cast<jint>(
|
||||
NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port,
|
||||
Common::Android::GetJString(env, username), Common::Android::GetJString(env, password),
|
||||
multiplayer->NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port,
|
||||
Common::Android::GetJString(env, username), Common::Android::GetJString(env, preferredGameName),
|
||||
preferredGameId,Common::Android::GetJString(env, password),
|
||||
Common::Android::GetJString(env, room_name), max_players));
|
||||
}
|
||||
|
||||
|
@ -929,70 +955,63 @@ JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayJoi
|
|||
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
|
||||
jstring username, jstring password) {
|
||||
return static_cast<jint>(
|
||||
NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port,
|
||||
multiplayer->NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port,
|
||||
Common::Android::GetJString(env, username), Common::Android::GetJString(env, password)));
|
||||
}
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayRoomInfo(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
return Common::Android::ToJStringArray(env, NetPlayRoomInfo());
|
||||
return Common::Android::ToJStringArray(env, multiplayer->NetPlayRoomInfo());
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsJoined(
|
||||
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
return NetPlayIsJoined();
|
||||
return multiplayer->NetPlayIsJoined();
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsHostedRoom(
|
||||
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
return NetPlayIsHostedRoom();
|
||||
return multiplayer->NetPlayIsHostedRoom();
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlaySendMessage(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj, jstring msg) {
|
||||
NetPlaySendMessage(Common::Android::GetJString(env, msg));
|
||||
multiplayer->NetPlaySendMessage(Common::Android::GetJString(env, msg));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayKickUser(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
|
||||
NetPlayKickUser(Common::Android::GetJString(env, username));
|
||||
multiplayer->NetPlayKickUser(Common::Android::GetJString(env, username));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayLeaveRoom(
|
||||
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
NetPlayLeaveRoom();
|
||||
multiplayer->NetPlayLeaveRoom();
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsModerator(
|
||||
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
return NetPlayIsModerator();
|
||||
return multiplayer->NetPlayIsModerator();
|
||||
}
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetBanList(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
return Common::Android::ToJStringArray(env, NetPlayGetBanList());
|
||||
return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetBanList());
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayBanUser(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
|
||||
NetPlayBanUser(Common::Android::GetJString(env, username));
|
||||
multiplayer->NetPlayBanUser(Common::Android::GetJString(env, username));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayUnbanUser(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
|
||||
NetPlayUnbanUser(Common::Android::GetJString(env, username));
|
||||
multiplayer->NetPlayUnbanUser(Common::Android::GetJString(env, username));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_yuzu_yuzu_1emu_NativeLibrary_netPlayInit(
|
||||
JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||
NetworkInit(&EmulationSession::GetInstance().System().GetRoomNetwork());
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
|
9
src/android/app/src/main/res/drawable/ic_ip.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_ip.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
|
||||
</vector>
|
10
src/android/app/src/main/res/drawable/ic_joined.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_joined.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
|
||||
</vector>
|
9
src/android/app/src/main/res/drawable/ic_multiplayer.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_multiplayer.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
|
||||
</vector>
|
220
src/android/app/src/main/res/layout/dialog_lobby_browser.xml
Normal file
220
src/android/app/src/main/res/layout/dialog_lobby_browser.xml
Normal file
|
@ -0,0 +1,220 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:elevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<Space
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/multiplayer_room_browser"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/refresh_button"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/refresh"
|
||||
app:icon="@drawable/ic_refresh" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/search_background"
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
app:cardCornerRadius="24dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/search_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_search"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/search_text"
|
||||
android:layout_width="180dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/multiplayer_search_public_lobbies"
|
||||
android:imeOptions="flagNoFullscreen"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/clear_button"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:src="@drawable/ic_clear"
|
||||
android:visibility="invisible"
|
||||
app:tint="?attr/colorOnSurfaceVariant"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_submit"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="110dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/submit" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/horizontalScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fadingEdge="horizontal"
|
||||
android:scrollbars="none"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/chips"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_hide_empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checkable="true"
|
||||
android:checked="false"
|
||||
android:text="@string/multiplayer_hide_empty_rooms"
|
||||
app:chipCornerRadius="16dp" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_hide_full"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checkable="true"
|
||||
android:checked="false"
|
||||
android:text="@string/multiplayer_hide_full_rooms"
|
||||
app:chipCornerRadius="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/room_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:contentDescription="@string/room_list"
|
||||
android:paddingBottom="16dp"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:src="@drawable/ic_refresh"
|
||||
android:alpha="0.5"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/multiplayer_no_rooms_found"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/multiplayer_tap_refresh_to_check_again"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/empty_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/refresh"
|
||||
app:icon="@drawable/ic_refresh" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
<ImageView
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
android:layout_height="114dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
|
@ -38,19 +38,45 @@
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Space
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_lobby_browser"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="175dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/multiplayer_public_room"
|
||||
app:cornerRadius="16dp"
|
||||
app:icon="@drawable/ic_search" />
|
||||
|
||||
<Space
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_join"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/multiplayer_join_room"
|
||||
app:icon="@drawable/ic_install"
|
||||
app:cornerRadius="16dp" />
|
||||
app:cornerRadius="16dp"
|
||||
app:icon="@drawable/ic_install" />
|
||||
|
||||
<Space
|
||||
android:layout_width="16dp"
|
||||
|
@ -58,13 +84,13 @@
|
|||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_create"
|
||||
style="@style/Widget.Material3.Button"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/multiplayer_create_room"
|
||||
app:icon="@drawable/ic_add"
|
||||
app:cornerRadius="16dp" />
|
||||
app:cornerRadius="16dp"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
@ -59,6 +59,21 @@
|
|||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:id="@+id/preferred_game_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:hint="@string/multiplayer_preferred_game_name">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/dropdown_preferred_game_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?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="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/multiplayer_password"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
9
src/android/app/src/main/res/layout/dropdown_item.xml
Normal file
9
src/android/app/src/main/res/layout/dropdown_item.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/dropdown_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1" />
|
113
src/android/app/src/main/res/layout/item_lobby_room.xml
Normal file
113
src/android/app/src/main/res/layout/item_lobby_room.xml
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:backgroundTint="?attr/colorSurfaceVariant"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/lock_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:contentDescription="@string/multiplayer_password_protected"
|
||||
android:src="@drawable/ic_lock"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorOnSurface" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/lock_icon"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/room_name"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Room Name" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/room_owner"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="14sp"
|
||||
tools:text="Hosted by: Owner" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/game_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:contentDescription="@string/multiplayer_game"
|
||||
android:src="@drawable/ic_controller" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/game_name"
|
||||
style="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textStyle="bold"
|
||||
tools:text="Game Name" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/ic_user"
|
||||
android:contentDescription="@string/multiplayer_player_count"
|
||||
app:tint="?attr/colorAccent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/player_count"
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textColor="?attr/colorAccent"
|
||||
tools:text="2/4" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
4
src/android/app/src/main/res/values-fr-rSN/strings.xml
Normal file
4
src/android/app/src/main/res/values-fr-rSN/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Eden</string>
|
||||
</resources>
|
|
@ -399,6 +399,11 @@
|
|||
<string name="use_custom_rtc_description">Allows you to set a custom real-time clock separate from your current system time.</string>
|
||||
<string name="set_custom_rtc">Set custom RTC</string>
|
||||
|
||||
<!-- Network settings strings -->
|
||||
<string name="web_token">Web Token</string>
|
||||
<string name="web_token_description">Web token used for creating public lobbies. It is a 48-character string containing only lowercase a-z.</string>
|
||||
<string name="network">Network</string>
|
||||
|
||||
<!-- Graphics settings strings -->
|
||||
<string name="frame_skipping">WIP: Frameskip</string>
|
||||
<string name="frame_skipping_description">Toggle frame skipping to improve performance by reducing the number of rendered frames. This feature is still being worked on and will be enabled in future releases.</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue