diff --git a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/HomeViews.kt b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/HomeViews.kt index fa3c8de85..5afc35773 100644 --- a/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/HomeViews.kt +++ b/src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/HomeViews.kt @@ -93,6 +93,17 @@ class HomeViews { const val ListImageSize = 150 const val GridImageSize = 300 + // --- small version badge bottom left + @Composable + private fun VersionBadge(modifier: Modifier = Modifier) { + Text( + text = "v" + org.kenjinx.android.BuildConfig.VERSION_NAME, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), + modifier = modifier.padding(8.dp) + ) + } + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun Home( @@ -125,45 +136,89 @@ class HomeViews { } } - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - .background(MaterialTheme.colorScheme.surface), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { + // --- Box around scaffold so we can overlay the badge + Box(Modifier.fillMaxSize()) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { Row( - horizontalArrangement = Arrangement.Start, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .background(MaterialTheme.colorScheme.surface), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(end = 8.dp) ) { - Box( - modifier = Modifier - .padding(end = 8.dp) - .size(56.dp) - .clip(RoundedCornerShape(8.dp)) - .background( - if (refreshUser && viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) { - Color.Transparent - } else { - MaterialTheme.colorScheme.surface - } - ) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(8.dp) - ) - .clickable { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(end = 8.dp) + ) { + Box( + modifier = Modifier + .padding(end = 8.dp) + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + if (refreshUser && viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) { + Color.Transparent + } else { + MaterialTheme.colorScheme.surface + } + ) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(8.dp) + ) + .clickable { + if (!isNavigating.value) { + isNavigating.value = true + val currentRoute = navController?.currentDestination?.route + if (currentRoute != "user") { + navController?.navigate("user") { + launchSingleTop = true + restoreState = true + } + } + CoroutineScope(Dispatchers.Main).launch { + delay(500) + isNavigating.value = false + } + } + }, + contentAlignment = Alignment.Center + ) { + if (refreshUser && viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) { + val pic = viewModel.mainViewModel.userViewModel.openedUser.userPicture + Image( + bitmap = BitmapFactory.decodeByteArray( + pic, + 0, + pic?.size ?: 0 + ).asImageBitmap(), + contentDescription = "user image", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } else { + Icon( + Icons.Filled.Person, + contentDescription = "User", + modifier = Modifier.size(28.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + + // Settings + IconButton( + onClick = { if (!isNavigating.value) { isNavigating.value = true val currentRoute = navController?.currentDestination?.route - if (currentRoute != "user") { - navController?.navigate("user") { + if (currentRoute != "settings") { + navController?.navigate("settings") { launchSingleTop = true restoreState = true } @@ -174,153 +229,91 @@ class HomeViews { } } }, - contentAlignment = Alignment.Center - ) { - if (refreshUser && viewModel.mainViewModel?.userViewModel?.openedUser?.userPicture?.isNotEmpty() == true) { - val pic = viewModel.mainViewModel.userViewModel.openedUser.userPicture - Image( - bitmap = BitmapFactory.decodeByteArray( - pic, - 0, - pic?.size ?: 0 - ).asImageBitmap(), - contentDescription = "user image", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } else { - Icon( - Icons.Filled.Person, - contentDescription = "User", - modifier = Modifier.size(28.dp), - tint = MaterialTheme.colorScheme.onSurface - ) + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(8.dp) + ) + ) { + Icon(Icons.Filled.Settings, contentDescription = "Settings") } - } - // Settings - IconButton( - onClick = { - if (!isNavigating.value) { - isNavigating.value = true - val currentRoute = navController?.currentDestination?.route - if (currentRoute != "settings") { - navController?.navigate("settings") { - launchSingleTop = true - restoreState = true - } - } - CoroutineScope(Dispatchers.Main).launch { - delay(500) - isNavigating.value = false - } - } - }, + } + + OutlinedTextField( + value = query.value, + onValueChange = { query.value = it }, modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(8.dp)) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(8.dp) - ) - ) { - Icon(Icons.Filled.Settings, contentDescription = "Settings") - } - - } - - OutlinedTextField( - value = query.value, - onValueChange = { query.value = it }, - modifier = Modifier - .weight(1f) - .height(56.dp), - placeholder = { - Text("Search...", modifier = Modifier.padding(bottom = 4.dp)) - }, - leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search") }, - singleLine = true, - shape = RoundedCornerShape(8.dp), - colors = TextFieldDefaults.outlinedTextFieldColors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline + .weight(1f) + .height(56.dp), + placeholder = { + Text("Search...", modifier = Modifier.padding(bottom = 4.dp)) + }, + leadingIcon = { Icon(Icons.Filled.Search, contentDescription = "Search") }, + singleLine = true, + shape = RoundedCornerShape(8.dp), + colors = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outline + ) ) - ) - } - }, - floatingActionButton = { - AnimatedVisibility(visible = isFabVisible) { - FloatingActionButton( - onClick = { - viewModel.requestReload() - viewModel.ensureReloadIfNecessary() - }, - shape = MaterialTheme.shapes.small, - containerColor = MaterialTheme.colorScheme.tertiary - ) { - Icon(Icons.Default.Refresh, contentDescription = "refresh") } - } - }, - floatingActionButtonPosition = FabPosition.End - ) { contentPadding -> - Column(modifier = Modifier.padding(contentPadding)) { - Box { - val list = remember { viewModel.gameList } - val isLoading = remember { viewModel.isLoading } + }, + floatingActionButton = { + AnimatedVisibility(visible = isFabVisible) { + FloatingActionButton( + onClick = { + viewModel.requestReload() + viewModel.ensureReloadIfNecessary() + }, + shape = MaterialTheme.shapes.small, + containerColor = MaterialTheme.colorScheme.tertiary + ) { + Icon(Icons.Default.Refresh, contentDescription = "refresh") + } + } + }, + floatingActionButtonPosition = FabPosition.End + ) { contentPadding -> + Column(modifier = Modifier.padding(contentPadding)) { + Box { + val list = remember { viewModel.gameList } + val isLoading = remember { viewModel.isLoading } - viewModel.filter(query.value) + viewModel.filter(query.value) - if (!isPreview) { - val settings = QuickSettings(viewModel.activity!!) - if (isLoading.value) { - Box(modifier = Modifier.fillMaxSize()) { - CircularProgressIndicator( - modifier = Modifier - .width(64.dp) - .align(Alignment.Center), - color = MaterialTheme.colorScheme.secondary, - trackColor = MaterialTheme.colorScheme.surfaceVariant - ) - } - } else { - if (settings.isGrid) { - val size = GridImageSize / Resources.getSystem().displayMetrics.density - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = (size + 4).dp), - modifier = Modifier - .fillMaxSize() - .padding(4.dp) - .nestedScroll(nestedScrollConnection), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - items(list) { - it.titleName?.apply { - if (this.isNotEmpty() && (query.value.trim() - .isEmpty() || this.lowercase(Locale.getDefault()) - .contains(query.value))) { - GridGameItem( - it, - viewModel, - showAppActions, - showLoading, - selectedModel, - showError - ) - } - } - } + if (!isPreview) { + val settings = QuickSettings(viewModel.activity!!) + if (isLoading.value) { + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator( + modifier = Modifier + .width(64.dp) + .align(Alignment.Center), + color = MaterialTheme.colorScheme.secondary, + trackColor = MaterialTheme.colorScheme.surfaceVariant + ) } } else { - LazyColumn(Modifier.fillMaxSize()) { - items(list) { - it.titleName?.apply { - if (this.isNotEmpty() && (query.value.trim() - .isEmpty() || this.lowercase(Locale.getDefault()) - .contains(query.value))) { - Box(modifier = Modifier.animateItemPlacement()) { - ListGameItem( + if (settings.isGrid) { + val size = GridImageSize / Resources.getSystem().displayMetrics.density + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = (size + 4).dp), + modifier = Modifier + .fillMaxSize() + .padding(4.dp) + .nestedScroll(nestedScrollConnection), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase(Locale.getDefault()) + .contains(query.value))) { + GridGameItem( it, viewModel, showAppActions, @@ -332,156 +325,182 @@ class HomeViews { } } } - } - } - } - } - } - } - - SimpleAlertDialog.Loading(showDialog = showLoading) - SimpleAlertDialog.Custom( - showDialog = openTitleUpdateDialog, - onDismissRequest = { openTitleUpdateDialog.value = false }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" - val name = viewModel.mainViewModel?.selected?.titleName ?: "" - TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose) - } - SimpleAlertDialog.Custom( - showDialog = openDlcDialog, - onDismissRequest = { openDlcDialog.value = false }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" - val name = viewModel.mainViewModel?.selected?.titleName ?: "" - DlcViews.Main(titleId, name, openDlcDialog, canClose) - } - } - - if (viewModel.mainViewModel?.loadGameModel?.value != null) - LaunchedEffect(viewModel.mainViewModel.loadGameModel.value) { - if (viewModel.mainViewModel.bootPath.value == - "gameItem_${viewModel.mainViewModel.loadGameModel.value!!.titleName}" - ) { - viewModel.mainViewModel.bootPath.value = null - - thread { - showLoading.value = true - val success = viewModel.mainViewModel.loadGame( - viewModel.mainViewModel.loadGameModel.value!!, - true, - viewModel.mainViewModel.forceNceAndPptc.value - ) ?: false - if (success == 1) { - launchOnUiThread { - viewModel.mainViewModel.navigateToGame() - } - } else { - if (success == -2) - showError.value = "Error loading update. Please re-add update file" - viewModel.mainViewModel.loadGameModel.value!!.close() - } - showLoading.value = false - } - } - } - - if (showAppActions.value) - ModalBottomSheet( - content = { - Row( - modifier = Modifier.padding(8.dp), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - if (showAppActions.value) { - IconButton(onClick = { - if (viewModel.mainViewModel?.selected != null) { - thread { - showLoading.value = true - val success = viewModel.mainViewModel.loadGame( - viewModel.mainViewModel.selected!! - ) - if (success == 1) { - launchOnUiThread { - viewModel.mainViewModel.navigateToGame() + } else { + LazyColumn(Modifier.fillMaxSize()) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase(Locale.getDefault()) + .contains(query.value))) { + Box(modifier = Modifier.animateItemPlacement()) { + ListGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel, + showError + ) + } + } } - } else { - if (success == -2) - showError.value = - "Error loading update. Please re-add update file" - viewModel.mainViewModel.selected!!.close() } - showLoading.value = false } } - }) { - Icon( - org.kenjinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), - contentDescription = "Run" - ) - } - val showAppMenu = remember { mutableStateOf(false) } - Box { - IconButton(onClick = { showAppMenu.value = true }) { - Icon(Icons.Filled.Menu, contentDescription = "Menu") - } - DropdownMenu( - expanded = showAppMenu.value, - onDismissRequest = { showAppMenu.value = false } - ) { - DropdownMenuItem( - text = { Text(text = "Clear PPTC Cache") }, - onClick = { - showAppMenu.value = false - viewModel.mainViewModel?.clearPptcCache( - viewModel.mainViewModel.selected?.titleId ?: "" - ) - } - ) - DropdownMenuItem( - text = { Text(text = "Purge Shader Cache") }, - onClick = { - showAppMenu.value = false - viewModel.mainViewModel?.purgeShaderCache( - viewModel.mainViewModel.selected?.titleId ?: "" - ) - } - ) - DropdownMenuItem( - text = { Text(text = "Delete All Cache") }, - onClick = { - showAppMenu.value = false - viewModel.mainViewModel?.deleteCache( - viewModel.mainViewModel.selected?.titleId ?: "" - ) - } - ) - DropdownMenuItem( - text = { Text(text = "Manage Updates") }, - onClick = { - showAppMenu.value = false - openTitleUpdateDialog.value = true - } - ) - DropdownMenuItem( - text = { Text(text = "Manage DLC") }, - onClick = { - showAppMenu.value = false - openDlcDialog.value = true - } - ) - } } } } - }, - onDismissRequest = { - showAppActions.value = false - selectedModel.value = null } + + SimpleAlertDialog.Loading(showDialog = showLoading) + SimpleAlertDialog.Custom( + showDialog = openTitleUpdateDialog, + onDismissRequest = { openTitleUpdateDialog.value = false }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" + val name = viewModel.mainViewModel?.selected?.titleName ?: "" + TitleUpdateViews.Main(titleId, name, openTitleUpdateDialog, canClose) + } + SimpleAlertDialog.Custom( + showDialog = openDlcDialog, + onDismissRequest = { openDlcDialog.value = false }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + val titleId = viewModel.mainViewModel?.selected?.titleId ?: "" + val name = viewModel.mainViewModel?.selected?.titleName ?: "" + DlcViews.Main(titleId, name, openDlcDialog, canClose) + } + } + + if (viewModel.mainViewModel?.loadGameModel?.value != null) + LaunchedEffect(viewModel.mainViewModel.loadGameModel.value) { + if (viewModel.mainViewModel.bootPath.value == + "gameItem_${viewModel.mainViewModel.loadGameModel.value!!.titleName}" + ) { + viewModel.mainViewModel.bootPath.value = null + + thread { + showLoading.value = true + val success = viewModel.mainViewModel.loadGame( + viewModel.mainViewModel.loadGameModel.value!!, + true, + viewModel.mainViewModel.forceNceAndPptc.value + ) ?: false + if (success == 1) { + launchOnUiThread { + viewModel.mainViewModel.navigateToGame() + } + } else { + if (success == -2) + showError.value = "Error loading update. Please re-add update file" + viewModel.mainViewModel.loadGameModel.value!!.close() + } + showLoading.value = false + } + } + } + + if (showAppActions.value) + ModalBottomSheet( + content = { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + if (showAppActions.value) { + IconButton(onClick = { + if (viewModel.mainViewModel?.selected != null) { + thread { + showLoading.value = true + val success = viewModel.mainViewModel.loadGame( + viewModel.mainViewModel.selected!! + ) + if (success == 1) { + launchOnUiThread { + viewModel.mainViewModel.navigateToGame() + } + } else { + if (success == -2) + showError.value = + "Error loading update. Please re-add update file" + viewModel.mainViewModel.selected!!.close() + } + showLoading.value = false + } + } + }) { + Icon( + org.kenjinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), + contentDescription = "Run" + ) + } + val showAppMenu = remember { mutableStateOf(false) } + Box { + IconButton(onClick = { showAppMenu.value = true }) { + Icon(Icons.Filled.Menu, contentDescription = "Menu") + } + DropdownMenu( + expanded = showAppMenu.value, + onDismissRequest = { showAppMenu.value = false } + ) { + DropdownMenuItem( + text = { Text(text = "Clear PPTC Cache") }, + onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.clearPptcCache( + viewModel.mainViewModel.selected?.titleId ?: "" + ) + } + ) + DropdownMenuItem( + text = { Text(text = "Purge Shader Cache") }, + onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.purgeShaderCache( + viewModel.mainViewModel.selected?.titleId ?: "" + ) + } + ) + DropdownMenuItem( + text = { Text(text = "Delete All Cache") }, + onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.deleteCache( + viewModel.mainViewModel.selected?.titleId ?: "" + ) + } + ) + DropdownMenuItem( + text = { Text(text = "Manage Updates") }, + onClick = { + showAppMenu.value = false + openTitleUpdateDialog.value = true + } + ) + DropdownMenuItem( + text = { Text(text = "Manage DLC") }, + onClick = { + showAppMenu.value = false + openDlcDialog.value = true + } + ) + } + } + } + } + }, + onDismissRequest = { + showAppActions.value = false + selectedModel.value = null + } + ) + + // --- Version badge bottom left above the entire content + VersionBadge( + modifier = Modifier.align(Alignment.BottomStart) ) + } // End of box } @OptIn(ExperimentalFoundationApi::class)