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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ app/.cxx/
# Kotlin 2.0
.kotlin/sessions
*.salive
kls_database.db

# Signing Configs
*.jks
Expand Down
34 changes: 34 additions & 0 deletions THIRD_PARTY_NOTICES
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Third-Party Notices

This file contains notices for third-party assets and libraries used in this project.

---

## Kenney Input Prompts

**Asset Pack:** Input Prompts
**Author:** Kenney (https://kenney.nl)
**Source:** https://kenney.nl/assets/input-prompts
**License:** CC0 1.0 Universal (Public Domain)

The input prompt icons used for gamepad, keyboard, and mouse button indicators
are from Kenney's Input Prompts asset pack. These icons are located in:
- `app/src/main/res/drawable/ic_input_xbox_*.xml` (Xbox controller icons)
- `app/src/main/res/drawable/ic_input_kbd_*.xml` (Keyboard and mouse icons)

While CC0 doesn't require attribution, we gratefully acknowledge Kenney's
excellent work in creating these assets.

---

## License Text: CC0 1.0 Universal

The person who associated a work with this deed has dedicated the work to the
public domain by waiving all of his or her rights to the work worldwide under
copyright law, including all related and neighboring rights, to the extent
allowed by law.

You can copy, modify, distribute and perform the work, even for commercial
purposes, all without asking permission.

Full license text: https://creativecommons.org/publicdomain/zero/1.0/
68 changes: 56 additions & 12 deletions app/src/main/java/app/gamenative/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,23 @@ import android.content.res.Configuration
import android.graphics.Color.TRANSPARENT
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.OrientationEventListener
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.CompositionLocalProvider
import androidx.lifecycle.lifecycleScope
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.request.CachePolicy
import androidx.lifecycle.lifecycleScope
import app.gamenative.events.AndroidEvent
import app.gamenative.service.SteamService
import app.gamenative.ui.PluviaMain
Expand All @@ -40,14 +34,18 @@ import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.IconDecoder
import app.gamenative.utils.IntentLaunchManager
import app.gamenative.utils.LocaleHelper
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.request.CachePolicy
import com.posthog.PostHog
import com.skydoves.landscapist.coil.LocalCoilImageLoader
import com.winlator.core.AppUtils
import com.winlator.inputcontrols.ControllerManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.EnumSet
import kotlin.math.abs
import kotlinx.coroutines.launch
import okio.Path.Companion.toOkioPath
import timber.log.Timber

Expand Down Expand Up @@ -123,14 +121,18 @@ class MainActivity : ComponentActivity() {
}

override fun onCreate(savedInstanceState: Bundle?) {
// Full immersive mode - transparent system bars for console-like experience
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(android.graphics.Color.rgb(30, 30, 30)),
navigationBarStyle = SystemBarStyle.light(TRANSPARENT, TRANSPARENT),
statusBarStyle = SystemBarStyle.dark(TRANSPARENT),
navigationBarStyle = SystemBarStyle.dark(TRANSPARENT),
)
super.onCreate(savedInstanceState)

// Apply immersive mode based on user preference
applyImmersiveMode()

// Initialize the controller management system
ControllerManager.getInstance().init(getApplicationContext());
ControllerManager.getInstance().init(getApplicationContext())

ContainerUtils.setContainerDefaults(applicationContext)

Expand Down Expand Up @@ -254,6 +256,9 @@ class MainActivity : ComponentActivity() {

override fun onResume() {
super.onResume()
// Re-apply immersive mode to ensure fullscreen persists
applyImmersiveMode()

// disable auto-stop when returning to foreground
SteamService.autoStopWhenIdle = false

Expand Down Expand Up @@ -317,7 +322,7 @@ class MainActivity : ComponentActivity() {
// Since LibraryScreen uses its own navigation system, this will need to be re-worked accordingly.
if (!eventDispatched) {
if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_DOWN) {
if (SteamService.isGameRunning){
if (SteamService.isGameRunning) {
PluviaApp.events.emit(AndroidEvent.BackPressed)
eventDispatched = true
}
Expand Down Expand Up @@ -361,6 +366,45 @@ class MainActivity : ComponentActivity() {
orientationSensorListener?.takeIf { it.canDetectOrientation() }?.enable()
}

/**
* Apply immersive mode for a full-screen experience.
* Must be called in multiple lifecycle methods to ensure bars stay hidden.
*/
private fun applyImmersiveMode() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to get some testing on a few different devices to make sure this functionality works here 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mostly feels like a hack, but the only way I could get it to consistently stay full screen. I have a hard time believing there isn’t a better way.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Use WindowInsetsController for Android 11+
window.setDecorFitsSystemWindows(false) // TODO: look into the proper way of doing this
window.insetsController?.let { controller ->
controller.hide(
android.view.WindowInsets.Type.statusBars() or
android.view.WindowInsets.Type.navigationBars(),
)
// Allow transient bars to appear on swipe from edge
controller.systemBarsBehavior =
android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
// Legacy approach for older Android versions
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
or android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
// Re-apply immersive mode when window gains focus to ensure bars stay hidden
if (hasFocus) {
applyImmersiveMode()
}
}

private fun setOrientationTo(orientation: Int, conformTo: EnumSet<Orientation>) {
// Log.d("MainActivity$index", "Setting orientation to conform")

Expand Down
25 changes: 10 additions & 15 deletions app/src/main/java/app/gamenative/PluviaApp.kt
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
package app.gamenative

import android.os.StrictMode
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.navigation.NavController
import app.gamenative.events.AndroidEvent
import app.gamenative.events.EventDispatcher
import app.gamenative.service.DownloadService
import app.gamenative.utils.ContainerMigrator
import app.gamenative.utils.IntentLaunchManager
import com.google.android.play.core.splitcompat.SplitCompatApplication
import com.posthog.PersonProfiles
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import com.winlator.inputcontrols.InputControlsManager
import com.winlator.widget.InputControlsView
import com.winlator.widget.TouchpadView
import com.winlator.widget.XServerView
import com.winlator.xenvironment.XEnvironment
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

// Add PostHog imports
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig

// Supabase imports
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.createSupabaseClient
import io.github.jan.supabase.postgrest.Postgrest
import io.github.jan.supabase.network.supabaseApi
import io.ktor.client.plugins.HttpTimeout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber

typealias NavChangedListener = NavController.OnDestinationChangedListener

Expand Down Expand Up @@ -70,7 +63,7 @@ class PluviaApp : SplitCompatApplication() {
ContainerMigrator.migrateLegacyContainersIfNeeded(
context = applicationContext,
onProgressUpdate = null,
onComplete = null
onComplete = null,
)
}

Expand Down Expand Up @@ -118,6 +111,8 @@ class PluviaApp : SplitCompatApplication() {
lateinit var supabase: SupabaseClient
private set

fun isSupabaseInitialized(): Boolean = ::supabase.isInitialized

// Initialize Supabase client
@OptIn(SupabaseInternal::class)
fun initSupabase() {
Expand All @@ -129,15 +124,15 @@ class PluviaApp : SplitCompatApplication() {

supabase = createSupabaseClient(
supabaseUrl = BuildConfig.SUPABASE_URL,
supabaseKey = BuildConfig.SUPABASE_KEY
supabaseKey = BuildConfig.SUPABASE_KEY,
) {
Timber.d("Configuring Supabase client")
httpConfig {
Timber.d("Setting up HTTP timeouts")
install(HttpTimeout) {
requestTimeoutMillis = 30_000 // overall call
connectTimeoutMillis = 15_000 // TCP handshake / TLS
socketTimeoutMillis = 30_000 // idle socket
requestTimeoutMillis = 30_000 // overall call
connectTimeoutMillis = 15_000 // TCP handshake / TLS
socketTimeoutMillis = 30_000 // idle socket
}
}
install(Postgrest)
Expand Down
22 changes: 10 additions & 12 deletions app/src/main/java/app/gamenative/PrefManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import app.gamenative.enums.AppTheme
import app.gamenative.ui.enums.AppFilter
import app.gamenative.ui.enums.HomeDestination
import app.gamenative.ui.enums.Orientation
import app.gamenative.Constants
import app.gamenative.ui.enums.PaneType
import com.materialkolor.PaletteStyle
import com.winlator.box86_64.Box86_64Preset
Expand Down Expand Up @@ -542,6 +541,16 @@ object PrefManager {
setPref(LIBRARY_FILTER, AppFilter.toFlags(value))
}

private val LIBRARY_SORT = intPreferencesKey("library_sort")
var librarySortOption: app.gamenative.ui.enums.SortOption
get() {
val value = getPref(LIBRARY_SORT, app.gamenative.ui.enums.SortOption.INSTALLED_FIRST.ordinal)
return app.gamenative.ui.enums.SortOption.fromOrdinal(value)
}
set(value) {
setPref(LIBRARY_SORT, value.ordinal)
}

/**
* Get or Set the last known Persona State. See [EPersonaState]
*/
Expand Down Expand Up @@ -733,17 +742,6 @@ object PrefManager {
setPref(EXTERNAL_STORAGE_PATH, value)
}

// Custom Games root (additional paths). Default path is provided by the app at runtime and isn't stored here.
private val CUSTOM_GAME_PATHS = stringPreferencesKey("custom_game_paths")
var customGamePaths: Set<String>
get() {
val value = getPref(CUSTOM_GAME_PATHS, "[]")
return try { Json.decodeFromString<Set<String>>(value) } catch (e: Exception) { emptySet() }
}
set(value) {
setPref(CUSTOM_GAME_PATHS, Json.encodeToString(value))
}

private val CUSTOM_GAME_MANUAL_FOLDERS = stringPreferencesKey("custom_game_manual_folders")
var customGameManualFolders: Set<String>
get() {
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/app/gamenative/data/LibraryItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ data class LibraryItem(
val isShared: Boolean = false,
val gameSource: GameSource = GameSource.STEAM,
val compatibilityStatus: GameCompatibilityStatus? = null,
val sizeBytes: Long = 0L,
val isInstalled: Boolean = false,
) {
val clientIconUrl: String
get() = when (gameSource) {
Expand Down
21 changes: 8 additions & 13 deletions app/src/main/java/app/gamenative/data/SteamFriend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.gamenative.ui.component.icons.VR
import app.gamenative.ui.theme.friendAwayOrSnooze
import app.gamenative.ui.theme.friendBlocked
import app.gamenative.ui.theme.friendInGame
import app.gamenative.ui.theme.friendInGameAwayOrSnooze
import app.gamenative.ui.theme.friendOffline
import app.gamenative.ui.theme.friendOnline
import app.gamenative.ui.theme.DarkColors
import `in`.dragonbra.javasteam.enums.EClientPersonaStateFlag
import `in`.dragonbra.javasteam.enums.EFriendRelationship
import `in`.dragonbra.javasteam.enums.EPersonaState
Expand Down Expand Up @@ -121,13 +116,13 @@ data class SteamFriend(

val statusColor: Color
get() = when {
isBlocked -> friendBlocked
isOffline -> friendOffline
isInGameAwayOrSnooze -> friendInGameAwayOrSnooze
isAwayOrSnooze -> friendAwayOrSnooze
isPlayingGame -> friendInGame
isOnline -> friendOnline
else -> friendOffline
isBlocked -> DarkColors.friendBlocked
isOffline -> DarkColors.friendOffline
isInGameAwayOrSnooze -> DarkColors.friendInGameAwayOrSnooze
isAwayOrSnooze -> DarkColors.friendAwayOrSnooze
isPlayingGame -> DarkColors.friendInGame
isOnline -> DarkColors.friendOnline
else -> DarkColors.friendOffline
}

val statusIcon: ImageVector?
Expand Down
Loading