diff --git a/app/src/main/java/app/gamenative/data/DownloadDisplayDetails.kt b/app/src/main/java/app/gamenative/data/DownloadDisplayDetails.kt new file mode 100644 index 0000000000..18fc67a57d --- /dev/null +++ b/app/src/main/java/app/gamenative/data/DownloadDisplayDetails.kt @@ -0,0 +1,10 @@ +package app.gamenative.ui.data + +data class DownloadDisplayDetails ( + val isInstalled: Boolean, + val isValidToDownload: Boolean, + val isDownloading: Boolean, + val hasPartialDownload: Boolean, + val downloadProgress: Float, + val isUpdatePending: Boolean +) diff --git a/app/src/main/java/app/gamenative/service/SteamService.kt b/app/src/main/java/app/gamenative/service/SteamService.kt index 5bcb9448f7..c0df595a27 100644 --- a/app/src/main/java/app/gamenative/service/SteamService.kt +++ b/app/src/main/java/app/gamenative/service/SteamService.kt @@ -174,6 +174,7 @@ import com.winlator.container.ContainerManager import app.gamenative.statsgen.StatType import app.gamenative.statsgen.StatsAchievementsGenerator import app.gamenative.statsgen.VdfParser +import app.gamenative.ui.data.Achievement import app.gamenative.utils.DownloadSpeedConfig import app.gamenative.utils.CustomGameScanner import java.nio.ByteBuffer @@ -3068,6 +3069,31 @@ class SteamService : Service(), IChallengeUrlChanged { } } + // Fetches achievements for a Steam app for display in the game details page. + suspend fun fetchAchievementsForDisplay(appId: Int): List? { + if (!isConnected) return null + return try { + val steamUser = instance?._steamUser ?: return null + val userStats = instance?._steamUserStats?.getUserStats(appId, steamUser.steamID!!)?.await() ?: return null + val baseIconUrl = SteamUtils.getBaseAchievementIconUrl(appId) + userStats.getExpandedAchievements().map { block -> + Achievement( + displayName = block.displayName ?: block.name ?: "", + name = block.name, + isUnlocked = block.isUnlocked, + description = block.description ?: "", + unlockTimestamp = block.unlockTimestamp, + hidden = block.hidden, + icon = if (!block.icon.isNullOrEmpty()) "$baseIconUrl${block.icon}" else "", + iconGray = if (!block.iconGray.isNullOrEmpty()) "$baseIconUrl${block.iconGray}" else null, + ) + } + } catch (e: Exception) { + Timber.e(e, "fetchAchievementsForDisplay failed for appId=$appId") + null + } + } + suspend fun generateAchievements(appId: Int, configDirectory: String) { val steamUser = instance!!._steamUser!! val userStats = instance?._steamUserStats!!.getUserStats(appId, steamUser.steamID!!).await() diff --git a/app/src/main/java/app/gamenative/ui/data/Achievement.kt b/app/src/main/java/app/gamenative/ui/data/Achievement.kt new file mode 100644 index 0000000000..90f5a948a9 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/data/Achievement.kt @@ -0,0 +1,18 @@ +package app.gamenative.ui.data + +data class Achievement( + val displayName: String, + val name: String?, + val isUnlocked: Boolean, + val description: String, + val unlockTimestamp: Int, + val hidden: Boolean, + val icon: String, + val iconGray: String? +){ + fun getFormattedUnlockTime(): String? { + if (unlockTimestamp == 0) return null + val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()) + return dateFormat.format(java.util.Date(unlockTimestamp * 1000L)) + } +} diff --git a/app/src/main/java/app/gamenative/ui/data/GameDisplayInfo.kt b/app/src/main/java/app/gamenative/ui/data/GameDisplayInfo.kt index 8aead2a8bf..b785b275ed 100644 --- a/app/src/main/java/app/gamenative/ui/data/GameDisplayInfo.kt +++ b/app/src/main/java/app/gamenative/ui/data/GameDisplayInfo.kt @@ -1,5 +1,7 @@ package app.gamenative.ui.data +import app.gamenative.data.GameSource + /** * Common data structure for displaying game information in the UI. * This allows both Steam and Custom Games to use the same UI layout. diff --git a/app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt b/app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt index 937d077b40..da3f10aa67 100644 --- a/app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt @@ -2,6 +2,7 @@ package app.gamenative.ui.screen.library +import android.annotation.SuppressLint import android.content.Intent import android.content.res.Configuration import app.gamenative.ui.screen.library.components.ambient.AmbientDownloadOverlay @@ -20,6 +21,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -50,17 +52,20 @@ import androidx.compose.material.icons.filled.CloudDownload import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -80,6 +85,8 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector @@ -108,7 +115,9 @@ import app.gamenative.ui.component.GamepadActionBar import app.gamenative.ui.component.GamepadButton import app.gamenative.ui.component.LoadingScreen import app.gamenative.ui.data.AppMenuOption +import app.gamenative.data.GameSource import app.gamenative.ui.data.GameDisplayInfo +import app.gamenative.ui.data.DownloadDisplayDetails import app.gamenative.ui.enums.AppOptionMenuType import app.gamenative.ui.internal.fakeAppInfo import app.gamenative.ui.screen.library.appscreen.AmazonAppScreen @@ -118,6 +127,10 @@ import app.gamenative.ui.screen.library.appscreen.GOGAppScreen import app.gamenative.ui.screen.library.appscreen.SteamAppScreen import app.gamenative.ui.screen.library.components.GameOptionsPanel import app.gamenative.ui.theme.PluviaTheme +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.ui.window.Dialog +import app.gamenative.ui.data.Achievement import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.coil.CoilImage import java.text.SimpleDateFormat @@ -460,11 +473,11 @@ fun AppScreen( // Get the appropriate screen model based on game source val screenModel = remember(libraryItem.gameSource) { when (libraryItem.gameSource) { - app.gamenative.data.GameSource.STEAM -> SteamAppScreen() - app.gamenative.data.GameSource.CUSTOM_GAME -> CustomGameAppScreen() - app.gamenative.data.GameSource.GOG -> GOGAppScreen() - app.gamenative.data.GameSource.EPIC -> EpicAppScreen() - app.gamenative.data.GameSource.AMAZON -> AmazonAppScreen() + GameSource.STEAM -> SteamAppScreen() + GameSource.CUSTOM_GAME -> CustomGameAppScreen() + GameSource.GOG -> GOGAppScreen() + GameSource.EPIC -> EpicAppScreen() + GameSource.AMAZON -> AmazonAppScreen() } } @@ -493,24 +506,31 @@ private fun formatBytes(bytes: Long): String { } } + + + @Composable internal fun AppScreenContent( modifier: Modifier = Modifier, displayInfo: GameDisplayInfo, - isInstalled: Boolean, - isValidToDownload: Boolean, - isDownloading: Boolean, - downloadProgress: Float, - hasPartialDownload: Boolean, - isUpdatePending: Boolean, + downloadDisplayDetails: DownloadDisplayDetails, downloadInfo: app.gamenative.data.DownloadInfo? = null, onDownloadInstallClick: () -> Unit, onPauseResumeClick: () -> Unit, onDeleteDownloadClick: () -> Unit, onUpdateClick: () -> Unit, onBack: () -> Unit = {}, + achievements: List? = null, vararg optionsMenu: AppMenuOption, ) { + + val isInstalled = downloadDisplayDetails.isInstalled + val isValidToDownload = downloadDisplayDetails.isValidToDownload + val isDownloading = downloadDisplayDetails.isDownloading + val hasPartialDownload = downloadDisplayDetails.hasPartialDownload + val downloadProgress = downloadDisplayDetails.downloadProgress + val isUpdatePending = downloadDisplayDetails.isUpdatePending + val context = LocalContext.current // reactive — recomposes when network state changes val hasInternet by NetworkMonitor.hasInternet.collectAsState() @@ -1094,6 +1114,10 @@ internal fun AppScreenContent( } } } + // Achievements Row + if (!achievements.isNullOrEmpty()) { + AchievementsRow(achievements = achievements) + } } } @@ -1212,6 +1236,197 @@ fun GameMigrationDialog( ) } +@SuppressLint("UnusedBoxWithConstraintsScope") +@Composable +private fun AchievementsRow( + achievements: List, +) { + // Temporarily this is Steam. We can expand later for other storefronts as they become available. + val unlockedCount = achievements.count { it.isUnlocked } + val totalCount = achievements.size + var showDialog by remember { mutableStateOf(false) } + val grayMatrix = remember { ColorMatrix().apply { setToSaturation(0f) } } + + val sortedAchievements = achievements.sortedWith( + compareByDescending { it.isUnlocked } + .thenByDescending { it.unlockTimestamp }, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = stringResource(R.string.achievements), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(bottom = 12.dp), + ) + + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier.padding(bottom = 36.dp), + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // Icons fill all space left over after the info column claims its natural width. + // BoxWithConstraints then tells us exactly how many 48dp icons (+ 8dp gaps) fit. + BoxWithConstraints(modifier = Modifier.weight(1f)) { + val iconSize = 48.dp + val spacing = 8.dp + val count = ((maxWidth + spacing) / (iconSize + spacing)) + .toInt() + .coerceIn(1, sortedAchievements.size) + Row(horizontalArrangement = Arrangement.spacedBy(spacing)) { + sortedAchievements.take(count).forEach { ach -> + val iconUrl = if (ach.isUnlocked) ach.icon.ifEmpty { ach.iconGray } else ach.iconGray ?: ach.icon.ifEmpty { null } + CoilImage( + imageModel = { iconUrl ?: "" }, + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + colorFilter = if (ach.isUnlocked) null else ColorFilter.colorMatrix(grayMatrix), + ), + modifier = Modifier + .size(iconSize) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer), + ) + } + } + } + + Column(horizontalAlignment = Alignment.End) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "$unlockedCount / $totalCount", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + // Give them a star for getting 100% completion + if(totalCount >= 1 && unlockedCount == totalCount) { + Icon( + imageVector = Icons.Filled.Star, + contentDescription = stringResource(R.string.achievements_complete), + tint = Color(0xFFFFD700), + modifier = Modifier.size(16.dp), + ) + } + } + Text( + text = stringResource(R.string.achievements), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + TextButton(onClick = { showDialog = true }) { + Text(stringResource(R.string.achievements_view_all)) + } + } + } + } + + if (showDialog) { + AchievementsDialog( + achievements = sortedAchievements, + onDismiss = { showDialog = false }, + ) + } +} + +@Composable +private fun AchievementsDialog( + achievements: List, + onDismiss: () -> Unit, +) { + val grayMatrix = remember { ColorMatrix().apply { setToSaturation(0f) } } + + Dialog(onDismissRequest = onDismiss) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surface, + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 600.dp), + ) { + Column { + Text( + text = stringResource(R.string.achievements_all_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp), + ) + HorizontalDivider() + LazyColumn(modifier = Modifier.weight(1f)) { + items(achievements) { ach -> + val iconUrl = if (ach.isUnlocked) ach.icon.ifEmpty { ach.iconGray } else ach.iconGray ?: ach.icon.ifEmpty { null } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + CoilImage( + imageModel = { iconUrl ?: "" }, + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + colorFilter = if (ach.isUnlocked) null else ColorFilter.colorMatrix(grayMatrix), + ), + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = ach.displayName ?: ach.name ?: "", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (!ach.description.isNullOrEmpty()) { + Text( + text = ach.description!!, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + val unlockTime = ach.getFormattedUnlockTime() + if (ach.isUnlocked && unlockTime != null) { + Text( + text = stringResource(R.string.achievements_unlocked_at, unlockTime), + style = MaterialTheme.typography.labelSmall, + color = PluviaTheme.colors.statusInstalled, + ) + } + } + } + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + } + } + HorizontalDivider() + TextButton( + onClick = onDismiss, + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 12.dp, vertical = 8.dp), + ) { + Text(stringResource(R.string.close)) + } + } + } + } +} + /*********** * PREVIEW * ***********/ @@ -1243,16 +1458,21 @@ private fun Preview_AppScreen() { lastPlayedText = null, playtimeText = null, ) + + val downloadDisplayDetails = DownloadDisplayDetails( + isInstalled = false, + isValidToDownload = true, + isDownloading = isDownloading, + downloadProgress = .50f, + hasPartialDownload = false, + isUpdatePending = false, + ) + PluviaTheme { Surface { AppScreenContent( displayInfo = displayInfo, - isInstalled = false, - isValidToDownload = true, - isDownloading = isDownloading, - downloadProgress = .50f, - hasPartialDownload = false, - isUpdatePending = false, + downloadDisplayDetails = downloadDisplayDetails, downloadInfo = null, onDownloadInstallClick = { isDownloading = !isDownloading }, onPauseResumeClick = { }, diff --git a/app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt b/app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt index 5510869c31..8043a90d0e 100644 --- a/app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt @@ -31,10 +31,12 @@ import app.gamenative.events.AndroidEvent import app.gamenative.ui.component.dialog.ContainerConfigDialog import app.gamenative.ui.data.AppMenuOption import app.gamenative.ui.data.GameDisplayInfo +import app.gamenative.ui.data.DownloadDisplayDetails import app.gamenative.ui.enums.AppOptionMenuType import app.gamenative.ui.util.ContainerConfigTransfer import app.gamenative.ui.util.SnackbarManager import app.gamenative.ui.component.dialog.LoadingDialog +import app.gamenative.ui.data.Achievement import app.gamenative.utils.BestConfigService import app.gamenative.utils.ContainerUtils import app.gamenative.utils.GameCompatibilityCache @@ -920,6 +922,32 @@ abstract class BaseAppScreen { mutableStateOf(hasPartialDownload(context, libraryItem)) } + var achievementsState by remember(libraryItem.gameId) { + mutableStateOf?>(null) + } + + // Achievements Fetching + LaunchedEffect(libraryItem.gameId) { + when(libraryItem.gameSource){ + GameSource.STEAM -> { + try { + achievementsState = withContext(Dispatchers.IO) { + app.gamenative.service.SteamService.fetchAchievementsForDisplay(libraryItem.gameId) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.e(e, "Failed to fetch achievements for gameId=${libraryItem.gameId}: ${e.message}") + achievementsState = null + } + } + GameSource.EPIC -> { } // Add later with Epic achievements + GameSource.GOG -> { } // Add later with GOG achievements + GameSource.AMAZON -> { } + GameSource.CUSTOM_GAME -> { } + } + } + val uiScope = rememberCoroutineScope() suspend fun performStateRefresh(includeUpdatePending: Boolean) { @@ -1156,11 +1184,11 @@ abstract class BaseAppScreen { // Get download info based on game source for progress tracking val downloadInfo = when (libraryItem.gameSource) { - app.gamenative.data.GameSource.STEAM -> app.gamenative.service.SteamService.getAppDownloadInfo(displayInfo.gameId) - app.gamenative.data.GameSource.EPIC -> app.gamenative.service.epic.EpicService.getDownloadInfo(displayInfo.gameId) - app.gamenative.data.GameSource.GOG -> app.gamenative.service.gog.GOGService.getDownloadInfo(displayInfo.gameId.toString()) - app.gamenative.data.GameSource.CUSTOM_GAME -> null // Custom games don't support downloads yet - app.gamenative.data.GameSource.AMAZON -> app.gamenative.service.amazon.AmazonService.getDownloadInfoByAppId(libraryItem.gameId) + GameSource.STEAM -> app.gamenative.service.SteamService.getAppDownloadInfo(displayInfo.gameId) + GameSource.EPIC -> app.gamenative.service.epic.EpicService.getDownloadInfo(displayInfo.gameId) + GameSource.GOG -> app.gamenative.service.gog.GOGService.getDownloadInfo(displayInfo.gameId.toString()) + GameSource.CUSTOM_GAME -> null // Custom games don't support downloads yet + GameSource.AMAZON -> app.gamenative.service.amazon.AmazonService.getDownloadInfoByAppId(libraryItem.gameId) } DisposableEffect(libraryItem.appId) { @@ -1182,15 +1210,18 @@ abstract class BaseAppScreen { } } - // Render the common UI - app.gamenative.ui.screen.library.AppScreenContent( - displayInfo = displayInfo, + val downloadDisplayDetails = DownloadDisplayDetails( isInstalled = isInstalledState, isValidToDownload = isValidToDownloadState, isDownloading = isDownloadingState, downloadProgress = downloadProgressState, hasPartialDownload = hasPartialDownloadState, - isUpdatePending = isUpdatePendingState, + isUpdatePending = isUpdatePendingState + ) + // Render the common UI + app.gamenative.ui.screen.library.AppScreenContent( + displayInfo = displayInfo, + downloadDisplayDetails = downloadDisplayDetails, downloadInfo = downloadInfo, onDownloadInstallClick = { onDownloadInstallClick(context, libraryItem, onClickPlay) @@ -1213,6 +1244,7 @@ abstract class BaseAppScreen { } }, onBack = onBack, + achievements = achievementsState, optionsMenu = optionsMenu.toTypedArray(), ) diff --git a/app/src/main/java/app/gamenative/utils/SteamUtils.kt b/app/src/main/java/app/gamenative/utils/SteamUtils.kt index 39bd275af3..705936f21a 100644 --- a/app/src/main/java/app/gamenative/utils/SteamUtils.kt +++ b/app/src/main/java/app/gamenative/utils/SteamUtils.kt @@ -49,6 +49,8 @@ import kotlin.io.path.setLastModifiedTime object SteamUtils { + fun getBaseAchievementIconUrl(appId: Int): String = "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/$appId/" + /** * True when a stored Steam session exists (offline-launch gate). * Matches GOG/Epic/Amazon AuthManager.hasStoredCredentials convention. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 2130fd6afe..1f2c3b23d3 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -501,6 +501,11 @@ Epic Offline-tilstand Start Epic-spil i offline-tilstand Præstation låst op + Præstationer + Se alle + Alle præstationer + Låst op: %s + Alle præstationer låst op Placering af præstationsnotifikation Øverst til venstre Øverst til højre diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5db19d70de..248a8259b8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -640,6 +640,11 @@ Epic Offline-Modus Epic-Spiele im Offlinemodus starten Erfolg freigeschaltet + Erfolge + Alle anzeigen + Alle Erfolge + Freigeschaltet: %s + Alle Erfolge freigeschaltet Position der Erfolgsbenachrichtigung Oben links Oben rechts diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 28c62ed009..ffc9394edb 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -713,6 +713,11 @@ Modo sin conexión de Epic Inicia juegos de Epic en modo sin conexión. Logro desbloqueado + Logros + Ver todos + Todos los logros + Desbloqueado: %s + Todos los logros desbloqueados Posición de notificación de logro Arriba izquierda Arriba derecha diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 306b40b207..211d77257e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -663,6 +663,11 @@ Mode Hors Ligne Epic Lancer les jeux Epic en mode hors ligne Succès débloqué + Succès + Voir tout + Tous les succès + Débloqué : %s + Tous les succès débloqués Position de la notification de succès En haut à gauche En haut à droite diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a0eaac4299..5a625be11a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -667,6 +667,11 @@ Modalità Offline Epic Avvia i giochi Epic in modalità offline Obiettivo sbloccato + Obiettivi + Vedi tutti + Tutti gli obiettivi + Sbloccato: %s + Tutti i traguardi sbloccati Posizione notifica obiettivo In alto a sinistra In alto a destra diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index df04f9a824..5a9225fb1d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,4 +1,4 @@ - + GameNative ユーザーログイン ツーファクター @@ -675,6 +675,11 @@ Steam オフラインモード Steam ゲームをオフライン モードで起動する 実績のロックが解除されました + 実績 + すべて表示 + すべての実績 + アンロック日時: %s + すべての実績を解除しました 達成通知位置 左上 右上 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3982dbeced..5803b39666 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -677,6 +677,11 @@ Epic 오프라인 모드 Epic 게임을 오프라인 모드로 실행 업적 달성 + 업적 + 모두 보기 + 모든 업적 + 달성: %s + 모든 도전 과제 달성 업적 알림 위치 좌측 상단 우측 상단 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cfd64a33c5..78f7bb06ad 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -676,6 +676,11 @@ Tryb Offline Epic Uruchamiaj gry Epic w trybie offline Osiągnięcie odblokowane + Osiągnięcia + Wyświetl wszystkie + Wszystkie osiągnięcia + Odblokowano: %s + Wszystkie osiągnięcia odblokowane Pozycja powiadomienia o osiągnięciu Lewy górny Prawy górny diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index dc86d4fe3e..a78634cd4a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -501,6 +501,11 @@ Modo Offline da Epic Iniciar jogos Epic no modo offline Conquista desbloqueada + Conquistas + Ver todas + Todas as conquistas + Desbloqueado em: %s + Todas as conquistas desbloqueadas Posição da notificação de conquista Superior esquerdo Superior direito diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 3c6546b203..7292812d0d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -668,6 +668,11 @@ Mod Offline Epic Pornește jocurile Epic în mod offline Realizare deblocată + Realizări + Vezi toate + Toate realizările + Deblocat: %s + Toate realizările deblocate Poziția notificării realizării Stânga sus Dreapta sus diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e2b6b057af..8eb48a018d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1327,6 +1327,11 @@ https://gamenative.app Декодер Windows Media Работа... Достижение разблокировано + Достижения + Просмотреть все + Все достижения + Разблокировано: %s + Все достижения разблокированы Позиция уведомления о достижении Верхний левый Верхний правый diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0b6ca5a1a5..08604a6ebc 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -662,6 +662,11 @@ Режим Офлайн Epic Запускати ігри Epic в офлайн режимі Досягнення розблоковано + Досягнення + Переглянути всі + Всі досягнення + Розблоковано: %s + Усі досягнення розблоковано Позиція сповіщення про досягнення Лівий верхній Правий верхній diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f1a35c5a5d..954173afc4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -658,6 +658,11 @@ Epic 离线模式 以离线模式启动 Epic 游戏 成就已解锁 + 成就 + 查看全部 + 所有成就 + 解锁时间:%s + 所有成就已解锁 成就通知位置 左上 右上 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e21b957383..70527b548e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -660,6 +660,11 @@ Epic 離線模式 以離線模式啟動 Epic 遊戲 成就已解鎖 + 成就 + 查看全部 + 所有成就 + 解鎖時間:%s + 所有成就已解鎖 成就通知位置 左上 右上 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a30875043..96c5267903 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -686,6 +686,11 @@ Steam Offline Mode Launch Steam games in offline mode Achievement Unlocked + Achievements + View all + All Achievements + Unlocked: %s + All achievements unlocked Achievement Notification Position Top Left Top Right