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
24 changes: 22 additions & 2 deletions app/src/main/app/db/PluviaDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import com.winlator.cmod.feature.stores.steam.db.dao.SteamAppDao
import com.winlator.cmod.feature.stores.steam.db.dao.SteamLicenseDao
import com.winlator.cmod.app.db.download.DownloadRecord
import com.winlator.cmod.app.db.download.DownloadRecordDao
import com.winlator.cmod.feature.stores.steam.utils.PrefManager
import com.winlator.cmod.feature.stores.gog.service.GOGService

const val DATABASE_NAME = "pluvia_database"

Expand All @@ -45,7 +47,7 @@ const val DATABASE_NAME = "pluvia_database"
DownloadingAppInfo::class,
DownloadRecord::class,
],
version = 8,
version = 9,
exportSchema = false,
)
@TypeConverters(
Expand Down Expand Up @@ -91,7 +93,7 @@ abstract class PluviaDatabase : RoomDatabase() {
context.applicationContext,
PluviaDatabase::class.java,
DATABASE_NAME,
).addMigrations(MIGRATION_6_7, MIGRATION_7_8)
).addMigrations(MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
.fallbackToDestructiveMigration(true)
.build()
.also { instance = it }
Expand All @@ -101,6 +103,24 @@ abstract class PluviaDatabase : RoomDatabase() {

fun getInstance(): PluviaDatabase = instance ?: throw IllegalStateException("PluviaDatabase not initialized")

private val MIGRATION_8_9 =
object : Migration(8, 9) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE gog_games_new (id TEXT NOT NULL, title TEXT NOT NULL, slug TEXT NOT NULL, download_size INTEGER NOT NULL, install_size INTEGER NOT NULL, is_installed INTEGER NOT NULL, install_path TEXT NOT NULL, image_url TEXT NOT NULL, hero_image_url TEXT NOT NULL, icon_url TEXT NOT NULL, description TEXT NOT NULL, release_date TEXT NOT NULL, developer TEXT NOT NULL, publisher TEXT NOT NULL, genres TEXT NOT NULL, languages TEXT NOT NULL, last_played INTEGER NOT NULL, play_time INTEGER NOT NULL, type INTEGER NOT NULL, exclude INTEGER NOT NULL DEFAULT 0, user_id TEXT NOT NULL, categories TEXT NOT NULL, PRIMARY KEY(id, user_id))")
if (PrefManager.gogCurrentAccountId.isEmpty() || !GOGService.isRunning) {
db.execSQL("DROP TABLE gog_games")
db.execSQL("ALTER TABLE gog_games_new RENAME TO gog_games")
} else {
db.execSQL("ALTER TABLE gog_games ADD COLUMN user_id TEXT NOT NULL DEFAULT ${PrefManager.gogCurrentAccountId}")
db.execSQL("ALTER TABLE gog_games ADD COLUMN categories TEXT NOT NULL DEFAULT ''")

db.execSQL("INSERT INTO gog_games_new SELECT * FROM gog_games")
db.execSQL("DROP TABLE gog_games")
db.execSQL("ALTER TABLE gog_games_new RENAME TO gog_games")
}
}
}

private val MIGRATION_7_8 =
object : Migration(7, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/feature/settings/stores/StoresFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ class StoresFragment : Fragment() {
CoroutineScope(Dispatchers.Main).launch {
val authResult = GOGAuthManager.authenticateWithCode(requireContext(), code)
if (authResult.isSuccess) {
GOGService.start(requireContext())
if (!GOGService.isRunning)
GOGService.start(requireContext())
else
GOGService.refreshLibrary(requireContext())
}
refresh()
}
Expand Down Expand Up @@ -115,6 +118,7 @@ class StoresFragment : Fragment() {
),
) {
StoresScreen(
context = context,
state = storeState,
serverOptions = serverOptions,
onSteamSignIn = { steamLoginLauncher.launch(Intent(requireContext(), SteamLoginActivity::class.java)) },
Expand Down
134 changes: 132 additions & 2 deletions app/src/main/feature/settings/stores/StoresScreen.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package com.winlator.cmod.feature.settings
import android.content.Context
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.FastOutSlowInEasing
Expand Down Expand Up @@ -47,7 +48,6 @@ import androidx.compose.material.icons.outlined.KeyboardArrowDown
import androidx.compose.material.icons.outlined.Language
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material.icons.outlined.Speed
import androidx.compose.material.icons.outlined.Wifi
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
Expand Down Expand Up @@ -78,10 +78,16 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.winlator.cmod.R
import com.winlator.cmod.feature.stores.gog.service.GOGAuthManager
import com.winlator.cmod.feature.stores.gog.service.GOGService
import com.winlator.cmod.feature.stores.steam.utils.PrefManager
import com.winlator.cmod.shared.ui.focus.rememberSettingsContentNav
import com.winlator.cmod.shared.ui.nav.LocalPaneNav
import com.winlator.cmod.shared.ui.nav.paneNavItem
import com.winlator.cmod.shared.ui.outlinedSwitchColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// Palette
private val BgDark = Color(0xFF11111C)
Expand Down Expand Up @@ -116,6 +122,7 @@ data class StoreState(

@Composable
fun StoresScreen(
context: Context,
state: StoreState,
serverOptions: List<Pair<Int, String>>,
onSteamSignIn: () -> Unit,
Expand Down Expand Up @@ -165,28 +172,37 @@ fun StoresScreen(
SectionLabel(stringResource(R.string.stores_accounts_connected_stores))

StoreCard(
context = context,
name = stringResource(R.string.stores_accounts_steam_integration_title),
icon = Icons.Outlined.Gamepad,
accentColor = Color(0xFF66C0F4),
isLoggedIn = state.isSteamLoggedIn,
onSignIn = onSteamSignIn,
onSignOut = onSteamSignOut,
onSwitch = null,
onGetAccounts = null
)
StoreCard(
context = context,
name = stringResource(R.string.preloader_platform_epic),
icon = Icons.Outlined.Gamepad,
accentColor = Color(0xFF8BAFD4),
isLoggedIn = state.isEpicLoggedIn,
onSignIn = onEpicSignIn,
onSignOut = onEpicSignOut,
onSwitch = null,
onGetAccounts = null
)
StoreCard(
context = context,
name = stringResource(R.string.preloader_platform_gog),
icon = Icons.Outlined.Gamepad,
accentColor = Color(0xFFA855F7),
isLoggedIn = state.isGogLoggedIn,
onSignIn = onGogSignIn,
onSignOut = onGogSignOut,
onSwitch = GOGService::switchAccount,
onGetAccounts = GOGAuthManager::getAccounts
)

SectionLabel(stringResource(R.string.stores_accounts_download_settings), modifier = Modifier.padding(top = 8.dp))
Expand Down Expand Up @@ -352,13 +368,16 @@ private fun SignOutConfirmDialog(
// Store card
@Composable
private fun StoreCard(
context: Context,
name: String,
icon: ImageVector,
accentColor: Color,
isLoggedIn: Boolean,
onSignIn: () -> Unit,
onSignOut: () -> Unit,
) {
onSwitch: ((String) -> Unit)?,
onGetAccounts: ((context: Context) -> Map<String, String>?)?,
) {
var showSignOutDialog by remember { mutableStateOf(false) }
if (showSignOutDialog) {
SignOutConfirmDialog(
Expand Down Expand Up @@ -479,6 +498,117 @@ private fun StoreCard(
textColor = if (isLoggedIn) DangerRed else accentColor,
onClick = if (isLoggedIn) ({ showSignOutDialog = true }) else onSignIn,
)

if (!isLoggedIn || onGetAccounts == null) return@Box
Box(modifier = Modifier.padding(4.dp)) {
ActionButton(
label = stringResource(R.string.common_ui_add_more),
textColor = accentColor,
onClick = {
onSignIn()
},
)
}

onGetAccounts.invoke(context)?.let {
if (it.isNotEmpty() && onSwitch != null) {
val usernames = it.map { itt -> itt.value }
SettingsDropDownMenu(
options = usernames,
selectedIndex = usernames.indexOf(it[PrefManager.gogCurrentAccountId]),
onOptionSelected = { selectedIndex ->
CoroutineScope(Dispatchers.IO).launch {
val userId = it.entries.firstOrNull { entry -> usernames[selectedIndex] == entry.value }?.key ?: return@launch
onSwitch(userId)
}
},
)
}
}
}
}
}

@Composable
private fun SettingsDropDownMenu(
options: List<String>,
selectedIndex: Int,
onOptionSelected: (Int) -> Unit,
accentColor: Color = Accent,
) {
var expanded by remember { mutableStateOf(false) }
val safeIndex = selectedIndex.coerceIn(0, (options.size - 1).coerceAtLeast(0))
val selectedLabel = options.getOrNull(safeIndex) ?: ""

Box {
Row(
modifier =
Modifier
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF222232))
.border(1.dp, accentColor.copy(alpha = 0.30f), RoundedCornerShape(8.dp))
.paneNavItem(
cornerRadius = 8.dp,
onActivate = { if (options.isNotEmpty()) expanded = true },
highlightColor = NavHighlight,
tapToSelect = true,
).padding(horizontal = 10.dp, vertical = 7.dp)
.widthIn(max = 180.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = selectedLabel,
color = accentColor,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
imageVector = Icons.Outlined.KeyboardArrowDown,
contentDescription = null,
tint = accentColor,
modifier = Modifier.size(14.dp),
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
shape = RoundedCornerShape(8.dp),
containerColor = Color(0xFF24243B),
border = BorderStroke(1.dp, CardBorder),
modifier = Modifier.widthIn(max = 260.dp),
) {
Column(
modifier =
Modifier
.heightIn(max = 260.dp)
.verticalScroll(rememberScrollState()),
) {
options.forEachIndexed { index, label ->
val isSelected = index == safeIndex
DropdownMenuItem(
text = {
Text(
text = label,
color = if (isSelected) accentColor else TextPrimary,
fontSize = 13.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
softWrap = true,
)
},
onClick = {
onOptionSelected(index)
expanded = false
},
modifier =
Modifier.background(
if (isSelected) accentColor.copy(alpha = 0.08f) else Color.Transparent,
),
)
}
}
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/feature/stores/gog/data/GOGGame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import com.winlator.cmod.feature.stores.steam.enums.AppType
* GOG Game entity for Room database
* Represents a game from the GOG platform
*/
@Entity(tableName = "gog_games")
@Entity(tableName = "gog_games", primaryKeys = ["id","user_id"])
data class GOGGame(
@PrimaryKey
@ColumnInfo("id")
val id: String,
@ColumnInfo("title")
Expand Down Expand Up @@ -51,6 +50,10 @@ data class GOGGame(
val type: AppType = AppType.game,
@ColumnInfo(name = "exclude", defaultValue = "0")
val exclude: Boolean = false,
@ColumnInfo("user_id")
val userId: String,
@ColumnInfo("categories")
val categories: String,
) {
companion object {
const val GOG_IMAGE_BASE_URL = "https://images.gog.com/images"
Expand Down
37 changes: 19 additions & 18 deletions app/src/main/feature/stores/gog/db/dao/GOGGameDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.winlator.cmod.feature.stores.gog.data.GOGGame
import com.winlator.cmod.feature.stores.steam.utils.PrefManager
import kotlinx.coroutines.flow.Flow

/**
Expand All @@ -26,32 +27,32 @@ interface GOGGameDao {
@Delete
suspend fun delete(game: GOGGame)

@Query("DELETE FROM gog_games WHERE id = :gameId")
suspend fun deleteById(gameId: String)
@Query("DELETE FROM gog_games WHERE id = :gameId AND user_id = :gogAccountId")
suspend fun deleteById(gameId: String, gogAccountId: String=PrefManager.gogCurrentAccountId)

@Query("SELECT * FROM gog_games WHERE id = :gameId")
suspend fun getById(gameId: String): GOGGame?
@Query("SELECT * FROM gog_games WHERE id = :gameId AND user_id = :gogAccountId")
suspend fun getById(gameId: String, gogAccountId: String=PrefManager.gogCurrentAccountId): GOGGame?

@Query("SELECT * FROM gog_games WHERE exclude = 0 ORDER BY title ASC")
fun getAll(): Flow<List<GOGGame>>
@Query("SELECT * FROM gog_games WHERE exclude = 0 AND user_id = :gogAccountId ORDER BY title ASC")
fun getAll(gogAccountId: String=PrefManager.gogCurrentAccountId): Flow<List<GOGGame>>

@Query("SELECT * FROM gog_games WHERE exclude = 0 ORDER BY title ASC")
suspend fun getAllAsList(): List<GOGGame>
@Query("SELECT * FROM gog_games WHERE exclude = 0 AND user_id = :gogAccountId ORDER BY title ASC")
suspend fun getAllAsList(gogAccountId: String=PrefManager.gogCurrentAccountId): List<GOGGame>

@Query("SELECT * FROM gog_games WHERE is_installed = :isInstalled AND exclude = 0 ORDER BY title ASC")
fun getByInstallStatus(isInstalled: Boolean): Flow<List<GOGGame>>
@Query("SELECT * FROM gog_games WHERE is_installed = :isInstalled AND exclude = 0 AND user_id = :gogAccountId ORDER BY title ASC")
fun getByInstallStatus(isInstalled: Boolean, gogAccountId: String=PrefManager.gogCurrentAccountId): Flow<List<GOGGame>>

@Query("SELECT * FROM gog_games WHERE exclude = 0 AND title LIKE '%' || :searchQuery || '%' ORDER BY title ASC")
fun searchByTitle(searchQuery: String): Flow<List<GOGGame>>
@Query("SELECT * FROM gog_games WHERE exclude = 0 AND user_id = :gogAccountId AND title LIKE '%' || :searchQuery || '%' ORDER BY title ASC")
fun searchByTitle(searchQuery: String, gogAccountId: String=PrefManager.gogCurrentAccountId): Flow<List<GOGGame>>

@Query("DELETE FROM gog_games WHERE is_installed = 0")
suspend fun deleteAllNonInstalledGames()
@Query("DELETE FROM gog_games WHERE is_installed = 0 AND user_id = :gogAccountId")
suspend fun deleteAllNonInstalledGames(gogAccountId: String=PrefManager.gogCurrentAccountId)

@Query("SELECT COUNT(*) FROM gog_games WHERE exclude = 0")
fun getCount(): Flow<Int>
@Query("SELECT COUNT(*) FROM gog_games WHERE exclude = 0 AND user_id = :gogAccountId")
fun getCount(gogAccountId: String=PrefManager.gogCurrentAccountId): Flow<Int>

@Query("SELECT id FROM gog_games")
suspend fun getAllGameIdsIncludingExcluded(): List<String>
@Query("SELECT id FROM gog_games WHERE user_id = :gogAccountId")
suspend fun getAllGameIdsIncludingExcluded(gogAccountId: String=PrefManager.gogCurrentAccountId): List<String>

/**
* Upsert GOG games while preserving install status and paths
Expand Down
Loading
Loading