mirror of
https://github.com/IllusionMan1212/NPS-Browser.git
synced 2026-10-24 18:12:38 +00:00
package details screen and other various fixes and improvements
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package com.illusionware.npsbrowser
|
||||
|
||||
sealed class Routes(val route: String) {
|
||||
object Home : Routes("home")
|
||||
object Onboarding : Routes("onboarding")
|
||||
object Settings : Routes("settings")
|
||||
object PackageDetails : Routes("package_details")
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -82,19 +83,24 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.illusionware.npsbrowser.R
|
||||
import com.illusionware.npsbrowser.Routes
|
||||
import com.illusionware.npsbrowser.data.ItemLayout
|
||||
import com.illusionware.npsbrowser.data.SettingsPreferences
|
||||
import com.illusionware.npsbrowser.model.ConsoleType
|
||||
import com.illusionware.npsbrowser.model.PackageItem
|
||||
import com.illusionware.npsbrowser.ui.components.*
|
||||
import com.illusionware.npsbrowser.ui.screens.OnBoarding
|
||||
import com.illusionware.npsbrowser.ui.screens.PackageDetails
|
||||
import com.illusionware.npsbrowser.ui.screens.SettingsScreen
|
||||
import com.illusionware.npsbrowser.ui.theme.ColorAccent
|
||||
import com.illusionware.npsbrowser.ui.theme.ColorOnPrimaryLight
|
||||
import com.illusionware.npsbrowser.ui.theme.NPSBrowserTheme
|
||||
import com.illusionware.npsbrowser.ui.theme.Typography
|
||||
import com.illusionware.npsbrowser.util.isNavigationBarNeedsScrim
|
||||
import com.illusionware.npsbrowser.viewmodels.OnboardingViewModel
|
||||
import com.illusionware.npsbrowser.viewmodels.PackageDetailsViewModel
|
||||
import com.illusionware.npsbrowser.viewmodels.PackageListViewModel
|
||||
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -116,6 +122,7 @@ class MainActivity : ComponentActivity() {
|
||||
val onboardingViewModel: OnboardingViewModel = viewModel(
|
||||
factory = OnboardingViewModel.Factory
|
||||
)
|
||||
val packageDetailsViewModel: PackageDetailsViewModel = viewModel()
|
||||
|
||||
val onboardingPrefs = onboardingViewModel.uiState.collectAsStateWithLifecycle().value
|
||||
val appTheme = runBlocking { settingsViewModel.getAppTheme() }
|
||||
@@ -135,29 +142,39 @@ class MainActivity : ComponentActivity() {
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = if (onboardingPrefs.seenOnboarding) "Home" else "Onboarding",
|
||||
startDestination = if (onboardingPrefs.seenOnboarding) Routes.Home.route else Routes.Onboarding.route,
|
||||
) {
|
||||
composable(route = "Home") {
|
||||
composable(Routes.Home.route) {
|
||||
HomePage(
|
||||
navigateToSettings = { navController.navigate("Settings") },
|
||||
navigateToSettings = { navController.navigate(Routes.Settings.route) },
|
||||
settingsPrefs = settingsPrefs,
|
||||
settingsViewModel = settingsViewModel,
|
||||
navigateToDetailsScreen = { item ->
|
||||
packageDetailsViewModel.setSelectedPackage(item)
|
||||
navController.navigate(Routes.PackageDetails.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(route = "Settings") {
|
||||
composable(Routes.Settings.route) {
|
||||
SettingsScreen(
|
||||
navigationGoBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
composable(route = "Onboarding") {
|
||||
composable(Routes.Onboarding.route) {
|
||||
OnBoarding(
|
||||
navigateToHome = {
|
||||
navController.popBackStack()
|
||||
navController.navigate("Home")
|
||||
navController.navigate(Routes.Home.route)
|
||||
},
|
||||
darkTheme = darkTheme,
|
||||
)
|
||||
}
|
||||
composable(Routes.PackageDetails.route) {
|
||||
PackageDetails(
|
||||
navigationGoBack = { navController.popBackStack() },
|
||||
packageDetailsViewModel = packageDetailsViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +185,8 @@ class MainActivity : ComponentActivity() {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomePage(
|
||||
navigateToSettings: () -> Unit = {},
|
||||
navigateToSettings: () -> Unit,
|
||||
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||
settingsPrefs: SettingsPreferences,
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
@@ -176,9 +194,22 @@ fun HomePage(
|
||||
var searchQuery by rememberSaveable { mutableStateOf("")}
|
||||
val searchBar = remember { FocusRequester() }
|
||||
var layoutDialogOpen by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
val systemUiController = rememberSystemUiController()
|
||||
val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||
|
||||
val tsvs = settingsViewModel.availableTsvs()
|
||||
var selectedBarItem by remember { mutableStateOf(ConsoleType.PSVITA) }
|
||||
var selectedBarItem by rememberSaveable { mutableStateOf(ConsoleType.PSVITA) }
|
||||
|
||||
LaunchedEffect(systemUiController) {
|
||||
systemUiController.setNavigationBarColor(
|
||||
color = if (context.isNavigationBarNeedsScrim()) {
|
||||
navbarScrimColor.copy(alpha = 0.7f)
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -272,7 +303,11 @@ fun HomePage(
|
||||
if (tsvs.isEmpty()) {
|
||||
NoTsvs(navigateToSettings = navigateToSettings)
|
||||
} else {
|
||||
PackageList(selectedBarItem, settingsPrefs = settingsPrefs)
|
||||
PackageList(
|
||||
navigateToDetailsScreen = navigateToDetailsScreen,
|
||||
type = selectedBarItem,
|
||||
settingsPrefs = settingsPrefs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +410,7 @@ fun NoTsvs(padding: PaddingValues = PaddingValues(0.dp), navigateToSettings: ()
|
||||
@Composable
|
||||
fun PackageList(
|
||||
type: ConsoleType = ConsoleType.PSVITA,
|
||||
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||
viewModel: PackageListViewModel = viewModel(
|
||||
factory = PackageListViewModel.Factory
|
||||
),
|
||||
@@ -429,7 +465,7 @@ fun PackageList(
|
||||
if (settingsPrefs.layout == ItemLayout.LIST.ordinal) {
|
||||
LazyColumn {
|
||||
items(packages) {
|
||||
PackageListItem(it)
|
||||
PackageListItem(it, navigateToDetailsScreen)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -438,7 +474,7 @@ fun PackageList(
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
) {
|
||||
items(packages) {
|
||||
PackageGridItem(it)
|
||||
PackageGridItem(it, navigateToDetailsScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,11 +484,15 @@ fun PackageList(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PackageListItem(item: PackageItem) {
|
||||
fun PackageListItem(
|
||||
item: PackageItem,
|
||||
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { /* TODO: */ }) {
|
||||
.clickable { navigateToDetailsScreen(item) }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
@@ -500,8 +540,8 @@ fun PackageListItem(item: PackageItem) {
|
||||
) {
|
||||
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
|
||||
Text(text = item.region)
|
||||
if (item.minFW.isNotEmpty()) {
|
||||
Text(text = String.format("%.2f", item.minFW.toFloat()))
|
||||
if (!item.minFW.isNullOrEmpty()) {
|
||||
Text(text = item.minFW)
|
||||
}
|
||||
if (item.pkgSize.isNotEmpty()) {
|
||||
Text(text = item.pkgSize)
|
||||
@@ -512,14 +552,16 @@ fun PackageListItem(item: PackageItem) {
|
||||
}
|
||||
}
|
||||
Column {
|
||||
if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
|
||||
if (item.pkgUrl.isNullOrEmpty()) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.no_url_icon),
|
||||
contentDescription = "No PKG URL",
|
||||
tint = Color(0xFFCB5A5A)
|
||||
)
|
||||
}
|
||||
if (item.consoleType != ConsoleType.PSX && (item.zRif.isEmpty() && item.rap.isEmpty())) {
|
||||
if (
|
||||
item.consoleType != ConsoleType.PSX &&
|
||||
(item.zRif.isNullOrEmpty() && item.rap.isNullOrEmpty())) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.no_key_icon),
|
||||
contentDescription = "No License Key",
|
||||
@@ -553,17 +595,20 @@ fun Tag(title: String, small: Boolean = false) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PackageGridItem(item: PackageItem) {
|
||||
fun PackageGridItem(
|
||||
item: PackageItem,
|
||||
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable { /* TODO: */ }
|
||||
.clickable { navigateToDetailsScreen(item) },
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
modifier = Modifier.padding(8.dp),
|
||||
) {
|
||||
Layout(
|
||||
content = {
|
||||
@@ -607,8 +652,8 @@ fun PackageGridItem(item: PackageItem) {
|
||||
) {
|
||||
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
|
||||
Text(text = item.region)
|
||||
if (item.minFW.isNotEmpty()) {
|
||||
Text(text = String.format("%.2f", item.minFW.toFloat()))
|
||||
if (!item.minFW.isNullOrEmpty()) {
|
||||
Text(text = item.minFW)
|
||||
}
|
||||
if (item.pkgSize.isNotEmpty()) {
|
||||
Text(text = item.pkgSize)
|
||||
@@ -616,14 +661,14 @@ fun PackageGridItem(item: PackageItem) {
|
||||
}
|
||||
}
|
||||
Row {
|
||||
if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
|
||||
if (item.pkgUrl.isNullOrEmpty()) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.no_url_icon),
|
||||
contentDescription = "No PKG URL",
|
||||
tint = Color(0xFFCB5A5A)
|
||||
)
|
||||
}
|
||||
if (item.zRif.isEmpty() && item.rap.isEmpty()) {
|
||||
if (item.zRif.isNullOrEmpty() && item.rap.isNullOrEmpty()) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.no_key_icon),
|
||||
contentDescription = "No License Key",
|
||||
|
||||
@@ -4,14 +4,14 @@ data class PackageItem(
|
||||
val titleId: String,
|
||||
val region: String,
|
||||
val name: String,
|
||||
val pkgUrl: String,
|
||||
val contentId: String,
|
||||
val modificationDate: String,
|
||||
val pkgUrl: String?,
|
||||
val contentId: String?,
|
||||
val modificationDate: String?,
|
||||
val pkgSize: String,
|
||||
val sha256: String,
|
||||
val zRif: String,
|
||||
val rap: String,
|
||||
val minFW: String,
|
||||
val sha256: String?,
|
||||
val zRif: String?,
|
||||
val rap: String?,
|
||||
val minFW: String?,
|
||||
|
||||
val consoleType: ConsoleType,
|
||||
val dataType: PackageItemType,
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.illusionware.npsbrowser.ui.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Eco
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.disabled
|
||||
import androidx.compose.ui.semantics.onClick
|
||||
import androidx.compose.ui.semantics.onLongClick
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun NPSFloatingActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
shape: Shape = FloatingActionButtonDefaults.shape,
|
||||
containerColor: Color = FloatingActionButtonDefaults.containerColor,
|
||||
contentColor: Color = MaterialTheme.colorScheme.primary,
|
||||
elevation: Dp = 6.dp,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.alpha(if (enabled) 1f else 0.5f)
|
||||
.then(if (enabled) Modifier.shadow(6.dp, shape) else Modifier.clip(shape))
|
||||
.then(if (enabled) Modifier.combinedClickable(
|
||||
onClick = { onClick() },
|
||||
onLongClick = {
|
||||
if (onLongClick != null) {
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
}
|
||||
) else Modifier)
|
||||
.semantics(
|
||||
mergeDescendants = true,
|
||||
properties = {
|
||||
if (!enabled) disabled()
|
||||
onClick { onClick(); true }
|
||||
if (onLongClick != null) {
|
||||
onLongClick { onLongClick(); true }
|
||||
}
|
||||
role = Role.Button
|
||||
},
|
||||
),
|
||||
color = containerColor,
|
||||
tonalElevation = 3.dp,
|
||||
shadowElevation = elevation,
|
||||
contentColor = contentColor,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun NPSFABPreview() {
|
||||
NPSFloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.Eco, contentDescription = "Eco")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package com.illusionware.npsbrowser.ui.screens
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.DownloadDone
|
||||
import androidx.compose.material.icons.filled.Public
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.illusionware.npsbrowser.R
|
||||
import com.illusionware.npsbrowser.activities.PlaceholderColor
|
||||
import com.illusionware.npsbrowser.data.ItemLayout
|
||||
import com.illusionware.npsbrowser.data.SettingsPreferences
|
||||
import com.illusionware.npsbrowser.data.Theme
|
||||
import com.illusionware.npsbrowser.ui.components.NPSAlertDialog
|
||||
import com.illusionware.npsbrowser.ui.components.NPSFloatingActionButton
|
||||
import com.illusionware.npsbrowser.ui.components.NPSIconButton
|
||||
import com.illusionware.npsbrowser.ui.theme.ColorSecondaryLight
|
||||
import com.illusionware.npsbrowser.ui.theme.Typography
|
||||
import com.illusionware.npsbrowser.viewmodels.PackageDetailsViewModel
|
||||
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PackageDetails(
|
||||
navigationGoBack: () -> Unit = {},
|
||||
settingsViewModel: SettingsViewModel = viewModel(
|
||||
factory = SettingsViewModel.Factory
|
||||
),
|
||||
packageDetailsViewModel: PackageDetailsViewModel,
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
val prefs = settingsViewModel.uiState.collectAsStateWithLifecycle().value
|
||||
|
||||
val item = packageDetailsViewModel.uiState.collectAsStateWithLifecycle().value.item!!
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState)},
|
||||
topBar = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
) {
|
||||
NPSIconButton(tooltip = "Go Back", onClick = navigationGoBack) {
|
||||
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Go Back" )
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
NPSFloatingActionButton(
|
||||
onClick = {
|
||||
if (!item.zRif.isNullOrEmpty()) {
|
||||
clipboard.setText(AnnotatedString(item.zRif))
|
||||
} else if (!item.rap.isNullOrEmpty()) {
|
||||
clipboard.setText(AnnotatedString(item.rap))
|
||||
}
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("License key copied to clipboard")
|
||||
}
|
||||
},
|
||||
enabled = !item.zRif.isNullOrEmpty() || !item.rap.isNullOrEmpty()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.key_copy_fill),
|
||||
contentDescription = "Copy License Key"
|
||||
)
|
||||
}
|
||||
NPSFloatingActionButton(
|
||||
onClick = { /* TODO: */ },
|
||||
onLongClick = { showBottomSheet = true },
|
||||
enabled = !item.pkgUrl.isNullOrEmpty()
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Download, contentDescription = "Download")
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
Column(Modifier.padding(padding)) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data("TODO")
|
||||
.crossfade(true)
|
||||
.error(R.drawable.default_game_icon)
|
||||
.build(),
|
||||
placeholder = ColorPainter(PlaceholderColor),
|
||||
contentDescription = "Package Icon",
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f / 1f)
|
||||
.fillMaxWidth(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "[${item.titleId}] ${item.name}",
|
||||
style = Typography.headlineSmall.copy(fontSize = 20.sp),
|
||||
fontWeight = FontWeight.Light,
|
||||
)
|
||||
if (!item.contentId.isNullOrEmpty()) {
|
||||
Spacer(modifier = Modifier.padding(4.dp))
|
||||
Text(
|
||||
text = item.contentId,
|
||||
style = Typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.padding(8.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (item.region.isNotEmpty()) {
|
||||
Info(
|
||||
label = item.region,
|
||||
iconDescription = "Region",
|
||||
icon = Icons.Filled.Public
|
||||
)
|
||||
}
|
||||
if (item.pkgSize.isNotEmpty()) {
|
||||
Info(
|
||||
label = item.pkgSize,
|
||||
iconDescription = "Package",
|
||||
icon = painterResource(id = R.drawable.package_ic),
|
||||
)
|
||||
}
|
||||
if (!item.minFW.isNullOrEmpty()) {
|
||||
Info(
|
||||
label = item.minFW,
|
||||
iconDescription = "Minimum Firmware",
|
||||
icon = painterResource(id = R.drawable.gear),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showBottomSheet) {
|
||||
OneTimeConfigBottomSheet(
|
||||
onDismiss = {
|
||||
scope.launch {
|
||||
sheetState.hide()
|
||||
showBottomSheet = false
|
||||
}
|
||||
},
|
||||
sheetState = sheetState,
|
||||
prefs = prefs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OneTimeConfigBottomSheet(
|
||||
sheetState: SheetState,
|
||||
onDismiss: () -> Unit,
|
||||
prefs: SettingsPreferences
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
windowInsets = WindowInsets(top = 0.dp),
|
||||
) {
|
||||
OneTimeConfigSheetContent(prefs)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Info(label: String, iconDescription: String, icon: Painter) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = iconDescription,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = Typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Info(label: String, iconDescription: String, icon: ImageVector) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = iconDescription,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = Typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Option(
|
||||
title: String,
|
||||
value: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 16.dp, horizontal = 24.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
|
||||
Text(text = value, color = ColorSecondaryLight, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OneTimeConfigSheetContent(prefs: SettingsPreferences) {
|
||||
var autoDecrypt by remember { mutableStateOf(prefs.pkg2zipAutoDecrypt) }
|
||||
var unpackInPlace by remember { mutableStateOf(prefs.unpackInDownload) }
|
||||
var deleteAfter by remember { mutableStateOf(prefs.deleteAfterUnpack) }
|
||||
var downloadDir by remember { mutableStateOf(Uri.decode(prefs.downloadDir)) }
|
||||
var decParamsDialog by remember { mutableStateOf(false) }
|
||||
var unpackDir by remember { mutableStateOf(Uri.decode(prefs.unpackDir)) }
|
||||
var pkg2zipParams by remember { mutableStateOf(prefs.pkg2zipParams) }
|
||||
var displayPkg2zipParams by remember { mutableStateOf(prefs.pkg2zipParams) }
|
||||
|
||||
val downloadDirPicker = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocumentTree(),
|
||||
) { dirUri ->
|
||||
if (dirUri != null) {
|
||||
downloadDir = dirUri.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val unpackDirPicker = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocumentTree(),
|
||||
) { dirUri ->
|
||||
if (dirUri != null) {
|
||||
unpackDir = dirUri.toString()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.navigationBarsPadding()
|
||||
) {
|
||||
Text(
|
||||
text = "One-time Configuration",
|
||||
textAlign = TextAlign.Center,
|
||||
style = Typography.titleLarge
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
FilterChip(
|
||||
label = { Text(text = "Decrypt automatically") },
|
||||
selected = autoDecrypt,
|
||||
onClick = { autoDecrypt = !autoDecrypt }
|
||||
)
|
||||
FilterChip(
|
||||
label = { Text(text = "Unpack in place") },
|
||||
selected = unpackInPlace,
|
||||
onClick = { unpackInPlace = !unpackInPlace },
|
||||
)
|
||||
FilterChip(
|
||||
label = { Text(text = "Delete after") },
|
||||
selected = deleteAfter,
|
||||
onClick = { deleteAfter = !deleteAfter },
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
Column {
|
||||
Option(
|
||||
title = "Download Directory",
|
||||
value = Uri.decode(downloadDir).ifEmpty { "None" },
|
||||
onClick = {
|
||||
downloadDirPicker.launch(Uri.EMPTY)
|
||||
}
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = !unpackInPlace
|
||||
) {
|
||||
Option(
|
||||
title = "Unpack Directory",
|
||||
value = Uri.decode(unpackDir).ifEmpty { "None" },
|
||||
onClick = {
|
||||
unpackDirPicker.launch(Uri.EMPTY)
|
||||
}
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = autoDecrypt
|
||||
) {
|
||||
Option(
|
||||
title = "Decryption Parameters",
|
||||
value = pkg2zipParams,
|
||||
onClick = { decParamsDialog = true }
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = { /*TODO: do the download*/ },
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.DownloadDone,
|
||||
contentDescription = "Download",
|
||||
Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text("Download")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (decParamsDialog) {
|
||||
NPSAlertDialog(
|
||||
onDismiss = {
|
||||
decParamsDialog = false
|
||||
displayPkg2zipParams = pkg2zipParams
|
||||
},
|
||||
title = "Decryption Parameters",
|
||||
buttons = {
|
||||
TextButton(onClick = {
|
||||
pkg2zipParams = displayPkg2zipParams
|
||||
decParamsDialog = false
|
||||
}) {
|
||||
Text(text = "OK")
|
||||
}
|
||||
}
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = displayPkg2zipParams,
|
||||
label = { Text(text = "Decryption Parameters") },
|
||||
onValueChange = {
|
||||
displayPkg2zipParams = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
|
||||
fun BottomSheetPreview() {
|
||||
val prefs = SettingsPreferences(
|
||||
Theme.SYSTEM.ordinal,
|
||||
ItemLayout.LIST.ordinal,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
"",
|
||||
true,
|
||||
)
|
||||
|
||||
Column {
|
||||
OneTimeConfigSheetContent(prefs = prefs)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
@@ -43,6 +45,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
@@ -98,12 +101,18 @@ fun SettingsScreen(
|
||||
Text(text = stringResource(id = R.string.title_activity_settings), style = Typography.titleLarge)
|
||||
}
|
||||
},
|
||||
contentWindowInsets = WindowInsets(bottom = 0.dp),
|
||||
) { padding ->
|
||||
Column(modifier = Modifier
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.padding(vertical = 16.dp)) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
Appearance(viewModel, prefs)
|
||||
TsvFiles(viewModel, prefs)
|
||||
Updates(viewModel, prefs)
|
||||
@@ -366,6 +375,7 @@ fun Downloads(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
|
||||
title = "Unpack Directory",
|
||||
value = Uri.decode(prefs.unpackDir).ifEmpty { "None" },
|
||||
icon = Icons.Outlined.Unarchive,
|
||||
enabled = !prefs.unpackInDownload,
|
||||
onClick = {
|
||||
unpackDirPicker.launch(Uri.EMPTY)
|
||||
}
|
||||
@@ -514,12 +524,14 @@ fun DialogSetting(
|
||||
value: String,
|
||||
icon: ImageVector?,
|
||||
onClick: () -> Unit,
|
||||
singleLine : Boolean = false
|
||||
singleLine : Boolean = false,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.alpha(if (enabled) 1f else 0.5f)
|
||||
.then(if (enabled) Modifier.clickable { onClick() } else Modifier)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
@@ -98,12 +98,12 @@ fun NPSBrowserTheme(
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
window.navigationBarColor = Color.Transparent.toArgb()
|
||||
// allows pages to extend to the status bar
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.illusionware.npsbrowser.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
// Borrowed from https://github.com/tachiyomiorg/tachiyomi/tree/master
|
||||
fun Context.isNavigationBarNeedsScrim(): Boolean {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
|
||||
InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.illusionware.npsbrowser.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
|
||||
// Borrowed from https://github.com/tachiyomiorg/tachiyomi/tree/master
|
||||
object InternalResourceHelper {
|
||||
fun getBoolean(context: Context, resName: String, defaultValue: Boolean): Boolean {
|
||||
val id = getResourceId(resName, "bool")
|
||||
return if (id != 0) {
|
||||
context.createPackageContext("android", 0).resources.getBoolean(id)
|
||||
} else {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource id from system resources
|
||||
* @param resName resource name to get
|
||||
* @param type resource type of [resName] to get
|
||||
* @return 0 if not available
|
||||
*/
|
||||
@SuppressLint("DiscouragedApi")
|
||||
private fun getResourceId(resName: String, type: String): Int {
|
||||
return Resources.getSystem().getIdentifier(resName, type, "android")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.illusionware.npsbrowser.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.illusionware.npsbrowser.model.PackageItem
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
data class SelectedPackageUiState(
|
||||
val item: PackageItem?,
|
||||
)
|
||||
|
||||
class PackageDetailsViewModel: ViewModel() {
|
||||
private val _uiState = MutableStateFlow(SelectedPackageUiState(null))
|
||||
val uiState: StateFlow<SelectedPackageUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun setSelectedPackage(item: PackageItem) {
|
||||
_uiState.value = SelectedPackageUiState(item)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.toLongOrDefault
|
||||
import java.io.InputStream
|
||||
|
||||
data class PackageListUiState(
|
||||
@@ -91,24 +92,64 @@ class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesReposit
|
||||
val tsv = tsvReader.readAllWithHeader(file!!)
|
||||
|
||||
tsv.forEach { row ->
|
||||
var size = ""
|
||||
val tempSize = row["File Size"]
|
||||
if (!tempSize.isNullOrEmpty()) {
|
||||
size = bytesIntoHumanReadable(tempSize.toLong())
|
||||
if (row["Name"].isNullOrEmpty()) {
|
||||
return@forEach
|
||||
}
|
||||
var size = ""
|
||||
var minFW: String? = null
|
||||
var pkgUrl: String? = null
|
||||
var zRif: String? = null
|
||||
var rap: String? = null
|
||||
var contentId: String? = null
|
||||
var sha256: String? = null
|
||||
var modificationDate: String? = null
|
||||
|
||||
val tempSize = row["File Size"]
|
||||
val tempFW = row["Required FW"]
|
||||
val tempUrl = row["PKG direct link"]
|
||||
val tempZRif = row["zRIF"]
|
||||
val tempRap = row["RAP"]
|
||||
val tempContentId = row["Content ID"]
|
||||
val tempsha256 = row["SHA256"]
|
||||
val tempDate = row["Last Modification Date"]
|
||||
if (!tempSize.isNullOrEmpty()) {
|
||||
size = bytesIntoHumanReadable(tempSize.toLongOrDefault(0))
|
||||
}
|
||||
if (!tempFW.isNullOrEmpty()) {
|
||||
minFW = String.format("%.2f", tempFW.toFloat())
|
||||
}
|
||||
if (!tempUrl.isNullOrEmpty() && tempUrl != "MISSING") {
|
||||
pkgUrl = tempUrl
|
||||
}
|
||||
if (!tempZRif.isNullOrEmpty() && tempZRif != "MISSING") {
|
||||
zRif = tempZRif
|
||||
}
|
||||
if (!tempRap.isNullOrEmpty() && tempRap != "MISSING") {
|
||||
rap = tempRap
|
||||
}
|
||||
if (!tempContentId.isNullOrEmpty()) {
|
||||
contentId = tempContentId
|
||||
}
|
||||
if (!tempsha256.isNullOrEmpty()) {
|
||||
sha256 = tempsha256
|
||||
}
|
||||
if (!tempDate.isNullOrEmpty()) {
|
||||
modificationDate = tempDate
|
||||
}
|
||||
|
||||
list.add(
|
||||
PackageItem(
|
||||
row["Title ID"] ?: "",
|
||||
row["Region"] ?: "",
|
||||
row["Name"] ?: "",
|
||||
row["PKG direct link"] ?: "",
|
||||
row["Content ID"] ?: "",
|
||||
row["Last Modification Date"] ?: "",
|
||||
row["Name"]?.trim() ?: "",
|
||||
pkgUrl,
|
||||
contentId,
|
||||
modificationDate,
|
||||
size,
|
||||
row["SHA256"] ?: "",
|
||||
row["zRIF"] ?: "",
|
||||
row["RAP"] ?: "",
|
||||
row["Required FW"] ?: "",
|
||||
sha256,
|
||||
zRif,
|
||||
rap,
|
||||
minFW,
|
||||
console,
|
||||
type,
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
<vector android:height="24dp" android:viewportHeight="256"
|
||||
android:viewportWidth="256" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#000000"
|
||||
android:pathData="m139.8,13.09a72.78,72.78 0,0 0,-69.17 95.25l-52.33,52.31a7.27,7.27 0,0 0,-2.13 5.16v36.36a7.27,7.27 0,0 0,7.27 7.27h12.95v-24.08a7.27,7.27 0,0 1,2.14 -5.14l52.31,-52.33A72.78,72.78 0,0 1,160.02 32.66,72.72 72.72,0 0,1 194.86,43 72.72,72.72 0,0 0,139.8 13.09ZM178.52,73.52a14.54,14.54 0,0 0,-9.95 3.66,14.54 14.54,0 0,1 -0.27,0.64 14.54,14.54 0,0 0,5.89 22,15.7 15.7,0 0,1 -0.98,-4.92 15.7,15.7 0,0 1,15.7 -15.69,15.7 15.7,0 0,1 0.16,0.02 14.54,14.54 0,0 0,-10.55 -5.7z" android:strokeWidth="0.908993"/>
|
||||
<path android:fillColor="#000000"
|
||||
android:pathData="M169.69,40.2A72.78,72.78 0,0 0,100.52 135.45l-52.33,52.32a7.27,7.27 0,0 0,-2.13 5.14v36.36a7.27,7.27 0,0 0,7.27 7.27h36.36a7.27,7.27 0,0 0,7.27 -7.27v-14.54h14.54a7.27,7.27 0,0 0,7.27 -7.27v-14.54h14.54a7.27,7.27 0,0 0,5.14 -2.13l8.69,-8.7A72.72,72.72 0,1 0,169.69 40.2ZM187.87,109.29a14.54,14.54 0,1 1,14.54 -14.54,14.54 14.54,0 0,1 -14.54,14.54z" android:strokeWidth="0.908993"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,4 @@
|
||||
<vector android:height="24dp" android:viewportHeight="256"
|
||||
android:viewportWidth="256" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#000000" android:pathData="M223.68,66.15 L135.68,18a15.88,15.88 0,0 0,-15.36 0l-88,48.17a16,16 0,0 0,-8.32 14v95.64a16,16 0,0 0,8.32 14l88,48.17a15.88,15.88 0,0 0,15.36 0l88,-48.17a16,16 0,0 0,8.32 -14L232,80.18A16,16 0,0 0,223.68 66.15ZM128,32l80.35,44L178.57,92.29l-80.35,-44ZM128,120L47.65,76 81.56,57.43l80.35,44ZM216,175.85h0l-80,43.79L136,133.83l32,-17.51L168,152a8,8 0,0 0,16 0L184,107.56l32,-17.51v85.76Z"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user