mirror of
https://github.com/izzy2lost/WeeU.git
synced 2026-07-06 00:19:59 -07:00
added cover downloading
This commit is contained in:
@@ -57,3 +57,4 @@ bin/graphicPacks/*
|
||||
.DS_Store
|
||||
.kilocode/mcp.json
|
||||
vcpkg-android-arm64.tar.gz
|
||||
src/android/app/src/main/assets/COVER_DOWNLOAD_README.md
|
||||
|
||||
@@ -247,4 +247,6 @@ dependencies {
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.coil.network.okhttp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"000500001010f300": "AFPE01",
|
||||
"0005000010144000": "ADVE01",
|
||||
"0005000010145e00": "AGRE52",
|
||||
"0005000010147b00": "ABBEVZ",
|
||||
"0005000010137c00": "AZEE01",
|
||||
"0005000010110f00": "AD2P78",
|
||||
"000500001012b200": "ADXPGD",
|
||||
"0005000010177700": "ADNP01",
|
||||
"000500001010db00": "AEME4Q",
|
||||
"0005000010188c00": "ADRP4Q",
|
||||
"000500001011c500": "APAE4Q",
|
||||
"0005000010137f00": "ARKE01",
|
||||
"0005000010129000": "WDKE08",
|
||||
"000500001010b400": "ASPE41",
|
||||
"000500001010d900": "AF3E69",
|
||||
"000500001011f200": "ASAE01",
|
||||
"000500001010e400": "AGPE01",
|
||||
"0005000010147e00": "AHKE01",
|
||||
"0005000010143300": "AHWE01",
|
||||
"000500001017d800": "BWPE01",
|
||||
"0005000010139900": "AJ5E41",
|
||||
"0005000010176f00": "BJDP41",
|
||||
"0005000010179100": "BJDE41",
|
||||
"00050000101b9800": "AJ6P41",
|
||||
"000500001010ae00": "AJDE41",
|
||||
"0005000010145200": "AJKE41",
|
||||
"00050000101abc00": "AXYE01",
|
||||
"0005000010193300": "BLKP6V",
|
||||
"0005000010135e00": "ALBE01",
|
||||
"000500001016ad00": "BTME01",
|
||||
"00050000101a5c00": "ALJP01",
|
||||
"0005000010132400": "ALMP01",
|
||||
"0005000010132b00": "ALME01",
|
||||
"0005000010168d00": "ALHP01",
|
||||
"000500001010d800": "AMDE69",
|
||||
"000500001010c800": "AURP01",
|
||||
"000500001010c700": "AURE01",
|
||||
"000500001010ed00": "AMKP01",
|
||||
"000500001010ec00": "AMKE01",
|
||||
"0005000010162e00": "ABAP01",
|
||||
"0005000010162d00": "ABAE01",
|
||||
"0005000010110800": "AMVP41",
|
||||
"000500001010af00": "AMVE41",
|
||||
"0005000010117200": "AHDP08",
|
||||
"0005000010118300": "AHDE08",
|
||||
"0005000010128400": "ANSP69",
|
||||
"0005000010128800": "ANSE69",
|
||||
"0005000010101d00": "ARPE01",
|
||||
"0005000010110a00": "ANGE01",
|
||||
"0005000010102100": "ALCP01",
|
||||
"0005000010102000": "ALCE01",
|
||||
"0005000010146300": "APCE01",
|
||||
"0005000010139400": "APFE5G",
|
||||
"000500001012be00": "AC3P01",
|
||||
"000500001010fc00": "ARBP41",
|
||||
"0005000010111400": "ARMP41",
|
||||
"000500001010b100": "ARME41",
|
||||
"000500001012b400": "ABHP08",
|
||||
"0005000010132c00": "ADCE01",
|
||||
"000500001010b200": "ASCE01",
|
||||
"0005000010178f00": "WKNPAY",
|
||||
"0005000010140400": "ASFP52",
|
||||
"000500001010b300": "AS2E8P",
|
||||
"0005000010175b00": "BSSE8P",
|
||||
"0005000010128f00": "ASNE8P",
|
||||
"0005000010146900": "AS5E52",
|
||||
"0005000010145d00": "ARDP01",
|
||||
"0005000010145c00": "ARDE01",
|
||||
"000500001018dd00": "AMAP01",
|
||||
"0005000010145000": "AXFP01",
|
||||
"0005000010144f00": "AXFE01",
|
||||
"000500001012c500": "ACRP01",
|
||||
"0005000010112000": "ACRE01",
|
||||
"0005000010143600": "BCZP01",
|
||||
"0005000010143500": "BCZE01",
|
||||
"000500001014e000": "ALAP01",
|
||||
"0005000010113300": "ASUE41",
|
||||
"0005000010135300": "ACMP01",
|
||||
"000500001012dc00": "ACME01",
|
||||
"000500001012ac00": "AS9P41",
|
||||
"0005000010173400": "AYEP52",
|
||||
"0005000010136700": "ATBP01",
|
||||
"0005000010133900": "ATBE01",
|
||||
"0005000010112b00": "AHBPC8",
|
||||
"0005000010140500": "AWCP41",
|
||||
"0005000010102300": "ASTE01",
|
||||
"0005000010137d00": "ANXE01",
|
||||
"0005000010144d00": "AWSE01",
|
||||
"000500001010e800": "AW3E52",
|
||||
"000500001011a700": "AZUP41",
|
||||
"000500001010dd00": "AZUE41",
|
||||
"00050000101c4c00": "AX5P01",
|
||||
"000500001010b600": "AYSE41",
|
||||
"0005000010149100": "AGRP52",
|
||||
"0005000010140000": "ANRP52",
|
||||
"00050000101c6500": "AALP01",
|
||||
"000500001017b500": "BXAP01",
|
||||
"000500001010f600": "ASSP41",
|
||||
"0005000010138800": "ASBP41",
|
||||
"0005000010157f00": "AAFP01",
|
||||
"0005000010157e00": "AAFE01",
|
||||
"0005000010172600": "AQUE01",
|
||||
"0005000010111000": "ABEP01",
|
||||
"000500001010ac00": "ABEE01",
|
||||
"0005000010146600": "ABVP01",
|
||||
"0005000010147500": "ABVE01",
|
||||
"0005000010180700": "AKBP01",
|
||||
"0005000010180600": "AKBE01",
|
||||
"00050000101b3d00": "BD3P4Q",
|
||||
"0005000010138300": "ARKP01",
|
||||
"000500001010ee00": "AF3P69",
|
||||
"000500001010ff00": "AGPP01",
|
||||
"0005000010162a00": "ATDP01",
|
||||
"000500001017d900": "BWPP01",
|
||||
"0005000010111a00": "AJSP01",
|
||||
"0005000010145b00": "AJKP41",
|
||||
"00050000101b5100": "AXYP01",
|
||||
"0005000010147a00": "AC2PVZ",
|
||||
"00050000101ca900": "BMSPVZ",
|
||||
"0005000010101e00": "ARPP01",
|
||||
"0005000010175d00": "AUNP01",
|
||||
"0005000010147900": "APCP01",
|
||||
"000500001017e500": "BPMP01",
|
||||
"0005000010193f00": "APGPVZ",
|
||||
"000500001012bd00": "AC3E01",
|
||||
"000500001010f100": "ARGP01",
|
||||
"000500001012b100": "ASNP8P",
|
||||
"0005000010176a00": "AGMP01",
|
||||
"0005000010111d00": "ASPP41",
|
||||
"000500001016d600": "AM9P52",
|
||||
"000500001019e600": "AZAP01",
|
||||
"00050000101d5000": "BPEP52",
|
||||
"000500001010f400": "ATRP52",
|
||||
"0005000010184e00": "AYCP01",
|
||||
"000500001010f700": "AYSP41",
|
||||
"0005000010172700": "AQUP01",
|
||||
"00050000101df500": "APKP01",
|
||||
"00050000101b0500": "AFXP01",
|
||||
"00050000101d1d00": "BRQPVZ",
|
||||
"000500001010f800": "AKNP01",
|
||||
"00050000101c4000": "BRDPNS",
|
||||
"00050000101aa500": "BKFPVZ",
|
||||
"000500001018cc00": "AFRPVZ",
|
||||
"00050000101be900": "ALRP01",
|
||||
"000500001016a700": "ALHE01",
|
||||
"00050000101a3600": "AVXP01",
|
||||
"00050000101bfc00": "BS5P52",
|
||||
"0005000010181f00": "BK7P52",
|
||||
"0005000010111f00": "AS2P8P",
|
||||
"0005000e10176900": "AGME01",
|
||||
"00050000101d9d00": "AUME01",
|
||||
"0005000010176900": "AGME01",
|
||||
"0005000010110700": "ASVP52",
|
||||
"000500001011a600": "ACAP52",
|
||||
"0005000010156100": "ACPP52",
|
||||
"0005000010136f00": "ADSP4Q",
|
||||
"000500001012da00": "AF6P52",
|
||||
"0005000010118400": "AFBP01",
|
||||
"0005000010140300": null,
|
||||
"000500001016c800": null,
|
||||
"000500001010f500": "AMEP69",
|
||||
"00050000101d0300": null,
|
||||
"0005000010133b00": "AS8P01",
|
||||
"000500001010fe00": "ATKP01",
|
||||
"00050000101a1600": null,
|
||||
"0005000010129400": "AWDP52",
|
||||
"000500001014e300": "AZBP01",
|
||||
"000500001010ab00": "ABTE01",
|
||||
"0005000010146800": "ACPE52",
|
||||
"000500001012ba00": "ADXE01",
|
||||
"0005000010111600": "AF6E52",
|
||||
"0005000010101a00": "APLE01",
|
||||
"000500001014b700": "ATWE01",
|
||||
"000500001010d700": "ASLE52",
|
||||
"0005000010134e00": "AS8E01",
|
||||
"00050000101b0400": "AFXE01",
|
||||
"000500001018dc00": "AMAE01",
|
||||
"000500001010fd00": "ATKE01",
|
||||
"000500001010e300": "AS9E41",
|
||||
"0005000010184d00": "AYCE01",
|
||||
"00050000101c4d00": "AX5E01",
|
||||
"00050000101df400": "APKE01",
|
||||
"0005000010113400": "AECP52",
|
||||
"0005000010111e00": "ASCP01",
|
||||
"000500001f600a00": "CNFE01",
|
||||
"000500001010f000": "AJDP41",
|
||||
"000500001f600b00": "CNFP01",
|
||||
"000500001019e500": "AZAE01",
|
||||
"0005000e10180700": "AKBP01",
|
||||
"0005000010144e00": "AWSP01",
|
||||
"0005000e10136f00": "ADSP4Q",
|
||||
"0005000010172b00": "AW9E01",
|
||||
"0005000010138e00": "ARME41",
|
||||
"0005000010131c00": "AW9P01",
|
||||
"0005000e10128400": "ANSP69",
|
||||
"0005000e10102100": "ALCP01",
|
||||
"0005000c1012b100": "ASNP8P",
|
||||
"000500021010ef01": "AZUE41",
|
||||
"0005000010159a00": "ASTE01",
|
||||
"0005000e1010ed00": "AMKP01",
|
||||
"0005000e10188c00": "ADRP4Q",
|
||||
"0005000e10181f00": "BK7P52",
|
||||
"0005000e1011b700": "AS9P41",
|
||||
"0005000e10144e00": "AWSP01",
|
||||
"0005000e10146100": "AFDE01",
|
||||
"0005000e10113400": "AECP52",
|
||||
"000500001016e200": "AXZE01",
|
||||
"0005000e10195d00": "APZP01",
|
||||
"0005000c10145000": "AXFP01",
|
||||
"000500001019cd00": "AGGE01",
|
||||
"0005000010119300": "APCE01",
|
||||
"0005000010132f00": "AHDP08",
|
||||
"000500001019d000": "ARKE01",
|
||||
"0005000010146100": "AFDE01",
|
||||
"0005000010109900": "AZAE01",
|
||||
"0005000010145600": "ADBE01",
|
||||
"0005000e10145000": "AXFP01",
|
||||
"0005000e10138e00": "ARME41",
|
||||
"00050000101b9900": "APHE01",
|
||||
"0005000010111300": "ABTP01",
|
||||
"0005000010131d00": "ASEJ01",
|
||||
"000500001014b800": "ARPP01"
|
||||
}
|
||||
@@ -89,6 +89,14 @@ namespace NativeEmulation
|
||||
if (!fs::exists(controllerProfileFolder))
|
||||
fs::create_directories(controllerProfileFolder);
|
||||
|
||||
// Initialize controller 0 with Wii U Gamepad if no profile exists
|
||||
const auto controller0Profile = ActiveSettings::GetConfigPath("controllerProfiles/controller0.xml");
|
||||
if (!fs::exists(controller0Profile))
|
||||
{
|
||||
InputManager::instance().set_controller(0, EmulatedController::Type::VPAD);
|
||||
InputManager::instance().save(0);
|
||||
}
|
||||
|
||||
const auto memorySearcherFolder = ActiveSettings::GetUserDataPath(L"memorySearcher").generic_wstring();
|
||||
if (!fs::exists(memorySearcherFolder))
|
||||
fs::create_directories(memorySearcherFolder);
|
||||
|
||||
@@ -19,13 +19,19 @@ class EmulationSettings(sharedPreferences: SharedPreferences) {
|
||||
var gamePadPosition by sharedPreferences.enumPref(GamePadPosition.RIGHT)
|
||||
}
|
||||
|
||||
enum class GameListViewMode {
|
||||
LIST,
|
||||
COVER
|
||||
}
|
||||
|
||||
class GuiSettings(sharedPreferences: SharedPreferences) {
|
||||
var language by sharedPreferences.stringPref(DEFAULT_LANGUAGE)
|
||||
var gameListViewMode by sharedPreferences.enumPref(GameListViewMode.LIST)
|
||||
}
|
||||
|
||||
class InputOverlaySettings(sharedPreferences: SharedPreferences) {
|
||||
var isVibrateOnTouchEnabled by sharedPreferences.booleanPref(false)
|
||||
var isOverlayEnabled by sharedPreferences.booleanPref(false)
|
||||
var isOverlayEnabled by sharedPreferences.booleanPref(true)
|
||||
var controllerIndex by sharedPreferences.intPref(0)
|
||||
var alpha by sharedPreferences.intPref(64)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
@@ -33,6 +34,17 @@ import androidx.compose.material.icons.filled.GraphicEq
|
||||
import androidx.compose.material.icons.filled.Headphones
|
||||
import androidx.compose.material.icons.filled.Layers
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||
import androidx.compose.material.icons.filled.ViewModule
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
@@ -69,6 +81,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -78,7 +91,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.izzy2lost.weeu.R
|
||||
import com.izzy2lost.weeu.common.ui.components.FilledSearchToolbar
|
||||
import com.izzy2lost.weeu.common.settings.GameListViewMode
|
||||
import com.izzy2lost.weeu.common.settings.SettingsManager
|
||||
import com.izzy2lost.weeu.gamelist.cover.CoverDownloadService
|
||||
import com.izzy2lost.weeu.common.ui.localization.tr
|
||||
import java.io.File
|
||||
import com.izzy2lost.weeu.nativeinterface.NativeGameTitles
|
||||
import com.izzy2lost.weeu.nativeinterface.NativeGameTitles.Game
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -107,6 +124,9 @@ fun GamesListScreen(
|
||||
val gameSearchQuery by gameListViewModel.filterText.collectAsStateWithLifecycle()
|
||||
val games by gameListViewModel.games.collectAsStateWithLifecycle()
|
||||
var fabExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var viewMode by remember { mutableStateOf(SettingsManager.guiSettings.gameListViewMode) }
|
||||
val context = LocalContext.current
|
||||
val coverService = remember { CoverDownloadService(context) }
|
||||
|
||||
val state = rememberPullToRefreshState()
|
||||
|
||||
@@ -127,7 +147,18 @@ fun GamesListScreen(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
topBar = {
|
||||
FilledSearchToolbar(
|
||||
actions = toolbarActions,
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
viewMode = if (viewMode == GameListViewMode.LIST) GameListViewMode.COVER else GameListViewMode.LIST
|
||||
SettingsManager.guiSettings.gameListViewMode = viewMode
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = if (viewMode == GameListViewMode.LIST) Icons.Filled.ViewModule else Icons.AutoMirrored.Filled.ViewList,
|
||||
contentDescription = tr("Toggle view")
|
||||
)
|
||||
}
|
||||
toolbarActions()
|
||||
},
|
||||
hint = tr("Search games"),
|
||||
query = gameSearchQuery,
|
||||
onValueChange = gameListViewModel::setFilterText
|
||||
@@ -165,6 +196,8 @@ fun GamesListScreen(
|
||||
) {
|
||||
GameList(
|
||||
games = games,
|
||||
viewMode = viewMode,
|
||||
coverService = coverService,
|
||||
setFavorite = gameListViewModel::setGameTitleFavorite,
|
||||
deleteShaderCaches = {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbar(tr("Shader caches removed")) }
|
||||
@@ -189,6 +222,8 @@ fun GamesListScreen(
|
||||
@Composable
|
||||
private fun GameList(
|
||||
games: List<Game>,
|
||||
viewMode: GameListViewMode,
|
||||
coverService: CoverDownloadService,
|
||||
startGame: (Game) -> Unit,
|
||||
goToGameDetails: (Game) -> Unit,
|
||||
goToGameEditProfile: (Game) -> Unit,
|
||||
@@ -200,28 +235,51 @@ private fun GameList(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxSize(),
|
||||
columns = GridCells.Adaptive(620.dp)
|
||||
columns = if (viewMode == GameListViewMode.COVER) GridCells.Adaptive(150.dp) else GridCells.Adaptive(620.dp)
|
||||
) {
|
||||
items(items = games, key = { it.titleId }) { game ->
|
||||
var showDeleteShaderConfirmationDialog by remember { mutableStateOf(false) }
|
||||
GameListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
game = game,
|
||||
onStartGame = startGame,
|
||||
onIsFavoriteChanged = { isFavorite ->
|
||||
setFavorite(game, isFavorite)
|
||||
},
|
||||
onEditGameProfile = {
|
||||
goToGameEditProfile(game)
|
||||
},
|
||||
onRemoveShaderCaches = { showDeleteShaderConfirmationDialog = true },
|
||||
onAboutTitle = {
|
||||
goToGameDetails(game)
|
||||
},
|
||||
onCreateShortcut = {
|
||||
createShortcut(game)
|
||||
},
|
||||
)
|
||||
|
||||
if (viewMode == GameListViewMode.COVER) {
|
||||
GameCoverItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
game = game,
|
||||
coverService = coverService,
|
||||
onStartGame = startGame,
|
||||
onIsFavoriteChanged = { isFavorite ->
|
||||
setFavorite(game, isFavorite)
|
||||
},
|
||||
onEditGameProfile = {
|
||||
goToGameEditProfile(game)
|
||||
},
|
||||
onRemoveShaderCaches = { showDeleteShaderConfirmationDialog = true },
|
||||
onAboutTitle = {
|
||||
goToGameDetails(game)
|
||||
},
|
||||
onCreateShortcut = {
|
||||
createShortcut(game)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
GameListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
game = game,
|
||||
onStartGame = startGame,
|
||||
onIsFavoriteChanged = { isFavorite ->
|
||||
setFavorite(game, isFavorite)
|
||||
},
|
||||
onEditGameProfile = {
|
||||
goToGameEditProfile(game)
|
||||
},
|
||||
onRemoveShaderCaches = { showDeleteShaderConfirmationDialog = true },
|
||||
onAboutTitle = {
|
||||
goToGameDetails(game)
|
||||
},
|
||||
onCreateShortcut = {
|
||||
createShortcut(game)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeleteShaderConfirmationDialog)
|
||||
ShaderCachesConfirmationDialog(
|
||||
@@ -475,3 +533,102 @@ private fun GameContextMenu(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun GameCoverItem(
|
||||
game: Game,
|
||||
coverService: CoverDownloadService,
|
||||
onStartGame: (Game) -> Unit,
|
||||
onIsFavoriteChanged: (Boolean) -> Unit,
|
||||
onEditGameProfile: () -> Unit,
|
||||
onRemoveShaderCaches: () -> Unit,
|
||||
onAboutTitle: () -> Unit,
|
||||
onCreateShortcut: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var contextMenuExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
var coverFile by remember { mutableStateOf<File?>(null) }
|
||||
|
||||
LaunchedEffect(game.titleId) {
|
||||
val titleIdHex = game.titleId.toString(16).uppercase().padStart(16, '0')
|
||||
println("Downloading cover for ${game.name} with titleId: $titleIdHex")
|
||||
val result = coverService.downloadCover(titleIdHex)
|
||||
println("Cover download result for ${game.name}: ${result?.absolutePath}")
|
||||
coverFile = result
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.padding(4.dp)
|
||||
.combinedClickable(
|
||||
onClick = { onStartGame(game) },
|
||||
onLongClick = { contextMenuExpanded = true }
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(0.71f)
|
||||
) {
|
||||
if (coverFile != null && coverFile!!.exists()) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(coverFile)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = game.name,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
} else {
|
||||
GameIcon(
|
||||
game = game,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (game.isFavorite) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
painter = painterResource(R.drawable.ic_favorite),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = game.name ?: "",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
GameContextMenu(
|
||||
expanded = contextMenuExpanded,
|
||||
onDismissRequest = { contextMenuExpanded = false },
|
||||
game = game,
|
||||
onIsFavoriteChanged = onIsFavoriteChanged,
|
||||
onEditGameProfile = onEditGameProfile,
|
||||
onRemoveShaderCaches = onRemoveShaderCaches,
|
||||
onAboutTitle = onAboutTitle,
|
||||
onCreateShortcut = onCreateShortcut,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.izzy2lost.weeu.gamelist.cover
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
|
||||
class CoverDownloadService(private val context: Context) {
|
||||
private val client = OkHttpClient()
|
||||
private val coverCacheDir = File(context.cacheDir, "covers").apply { mkdirs() }
|
||||
|
||||
init {
|
||||
GameTitleMapper.initialize(context)
|
||||
}
|
||||
|
||||
suspend fun downloadCover(titleIdHex: String, region: String = "US"): File? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val coverFile = getCoverFile(titleIdHex)
|
||||
if (coverFile.exists()) {
|
||||
println("Cover already exists: ${coverFile.absolutePath}")
|
||||
return@withContext coverFile
|
||||
}
|
||||
|
||||
// Get game ID from mapper (this is the full 6-character GameTDB ID like ALZP01)
|
||||
val gameId = GameTitleMapper.getGameCodeFromTitleId(titleIdHex)
|
||||
println("Title ID: $titleIdHex -> Game ID: $gameId")
|
||||
|
||||
if (gameId != null) {
|
||||
// If the game ID is already 6 characters, use it directly
|
||||
// Otherwise, append region code
|
||||
val fullGameId = if (gameId.length >= 6) {
|
||||
gameId
|
||||
} else {
|
||||
// GameTDB format: 4-char code + 2-char region (e.g., ALZP01 for US, ALZP02 for EU)
|
||||
val regionCode = when(region) {
|
||||
"US", "USA" -> "01"
|
||||
"EU", "EUR" -> "02"
|
||||
"JP", "JPN" -> "00"
|
||||
else -> "01"
|
||||
}
|
||||
gameId + regionCode
|
||||
}
|
||||
|
||||
println("Full game ID: $fullGameId")
|
||||
|
||||
// Try multiple URL formats, prioritizing custom upscaled 3D covers from GitHub
|
||||
val urls = listOf(
|
||||
// Custom upscaled 3D covers from GitHub (preferred - 4x quality)
|
||||
"https://raw.githubusercontent.com/izzy2lost/WeeU-Covers/main/3D/$fullGameId.png",
|
||||
// GameTDB 3D covers (fallback)
|
||||
"https://art.gametdb.com/wiiu/cover3D/US/$fullGameId.png",
|
||||
"https://art.gametdb.com/wiiu/cover3D/EN/$fullGameId.png",
|
||||
"https://art.gametdb.com/wiiu/cover3D/EU/$fullGameId.png",
|
||||
"https://art.gametdb.com/wiiu/cover3D/JA/$fullGameId.png",
|
||||
// Full HQ covers (fallback)
|
||||
"https://art.gametdb.com/wiiu/coverfullHQ/US/$fullGameId.png",
|
||||
"https://art.gametdb.com/wiiu/coverfullHQ/EN/$fullGameId.png",
|
||||
"https://art.gametdb.com/wiiu/coverfullHQ/EU/$fullGameId.png",
|
||||
// Regular covers (last resort)
|
||||
"https://art.gametdb.com/wiiu/cover/US/$fullGameId.png",
|
||||
"https://art.gametdb.com/wiiu/cover/EN/$fullGameId.png",
|
||||
"https://art.gametdb.com/wiiu/cover/EU/$fullGameId.png"
|
||||
)
|
||||
|
||||
for (url in urls) {
|
||||
try {
|
||||
println("Trying URL: $url")
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("User-Agent", "WeeU-Android/1.0")
|
||||
.build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
println("Response code: ${response.code}")
|
||||
if (response.isSuccessful) {
|
||||
response.body?.byteStream()?.use { input ->
|
||||
coverFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
println("Successfully downloaded cover from: $url")
|
||||
println("Saved to: ${coverFile.absolutePath}")
|
||||
return@withContext coverFile
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Error downloading from $url: ${e.message}")
|
||||
// Try next URL
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
println("Failed to download cover from any URL for game ID: $fullGameId")
|
||||
} else {
|
||||
println("Could not determine game ID for title: $titleIdHex")
|
||||
}
|
||||
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getCoverFile(titleId: String): File {
|
||||
return File(coverCacheDir, "$titleId.png")
|
||||
}
|
||||
|
||||
fun hasCover(titleId: String): Boolean {
|
||||
return getCoverFile(titleId).exists()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.izzy2lost.weeu.gamelist.cover
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
|
||||
object GameTitleMapper {
|
||||
private var titleIdToGameIdMap: Map<String, String?>? = null
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun initialize(context: Context) {
|
||||
if (titleIdToGameIdMap != null) {
|
||||
println("GameTitleMapper already initialized")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Load title ID to game ID mapping
|
||||
println("Loading titleid_to_gameid_mapping.json from assets...")
|
||||
val mapInputStream = context.assets.open("titleid_to_gameid_mapping.json")
|
||||
val mapReader = BufferedReader(InputStreamReader(mapInputStream))
|
||||
val mapJsonString = mapReader.use { it.readText() }
|
||||
titleIdToGameIdMap = json.decodeFromString(mapJsonString)
|
||||
println("Loaded ${titleIdToGameIdMap?.size} title ID mappings")
|
||||
} catch (e: Exception) {
|
||||
println("Error loading game data: ${e.message}")
|
||||
e.printStackTrace()
|
||||
titleIdToGameIdMap = emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun getGameCodeFromTitleId(titleIdHex: String): String? {
|
||||
val normalizedId = titleIdHex.lowercase().padStart(16, '0')
|
||||
println("Looking up title ID: $normalizedId")
|
||||
|
||||
// First, try to get the game ID from the mapping file
|
||||
val gameId = titleIdToGameIdMap?.get(normalizedId)
|
||||
if (gameId != null) {
|
||||
println("Found game ID in mapping: $gameId")
|
||||
return gameId
|
||||
}
|
||||
|
||||
// If not found in mapping, try to extract from title ID
|
||||
println("Game ID not found in mapping, attempting extraction")
|
||||
val extractedCode = extractGameCode(normalizedId)
|
||||
println("Extracted game code: $extractedCode")
|
||||
|
||||
return extractedCode
|
||||
}
|
||||
|
||||
private fun extractGameCode(titleIdHex: String): String? {
|
||||
if (titleIdHex.length != 16) return null
|
||||
|
||||
// Wii U title ID format: 0005000010XXXXXX
|
||||
// GameTDB format: 6-char game ID (e.g., ALZP01)
|
||||
// The game code is encoded in bytes 10-13 (hex positions 8-15)
|
||||
|
||||
// Extract last 8 hex digits: positions 8-15
|
||||
val gameIdHex = titleIdHex.substring(8, 16)
|
||||
|
||||
// Convert hex to bytes and try to extract ASCII game code
|
||||
return try {
|
||||
// Take first 8 hex chars (4 bytes) for the game code
|
||||
val codeBytes = gameIdHex.substring(0, 8).chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
|
||||
// Convert to ASCII string
|
||||
val gameCode = String(codeBytes, Charsets.US_ASCII)
|
||||
.filter { it.isLetterOrDigit() }
|
||||
|
||||
// GameTDB format is typically 6 characters (4 game + 2 region)
|
||||
// For now, just return the 4-character game code
|
||||
// The region will be added by the download service
|
||||
if (gameCode.length >= 4) {
|
||||
gameCode.take(4).uppercase()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ espresso-core = "3.7.0"
|
||||
okhttp = "5.1.0"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
kotlinx-gettext = "0.7.0"
|
||||
coil = "3.0.4"
|
||||
|
||||
[libraries]
|
||||
aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
|
||||
@@ -42,6 +43,8 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "okhttp" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||
kotlinx-gettext = { module = "name.kropp.kotlinx-gettext:kotlinx-gettext", version.ref = "kotlinx-gettext" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
|
||||
|
||||
[plugins]
|
||||
aboutlibraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" }
|
||||
|
||||
@@ -152,7 +152,7 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser)
|
||||
|
||||
notification.controller_profiles = overlay_node.get("ControllerProfiles", true);
|
||||
notification.controller_battery = overlay_node.get("ControllerBattery", true);
|
||||
notification.shader_compiling = overlay_node.get("ShaderCompiling", true);
|
||||
notification.shader_compiling = overlay_node.get("ShaderCompiling", false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -167,7 +167,7 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser)
|
||||
|
||||
notification.controller_profiles = graphic.get("OverlayControllerProfiles", true);
|
||||
notification.controller_battery = graphic.get("OverlayControllerBattery", true);
|
||||
notification.shader_compiling = graphic.get("ShaderCompiling", true);
|
||||
notification.shader_compiling = graphic.get("ShaderCompiling", false);
|
||||
}
|
||||
|
||||
auto notification_node = graphic.get("Notification");
|
||||
|
||||
@@ -416,7 +416,7 @@ struct CemuConfig
|
||||
sint32 text_scale = 100;
|
||||
bool controller_profiles = true;
|
||||
bool controller_battery = false;
|
||||
bool shader_compiling = true;
|
||||
bool shader_compiling = false;
|
||||
bool friends = true;
|
||||
} notification{};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user