Compare commits

..
11 Commits
90 changed files with 4288 additions and 1549 deletions
-87
View File
@@ -1,87 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('keystore.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
namespace 'com.illusionware.npsbrowser'
compileSdkVersion 33
defaultConfig {
applicationId "com.illusionware.npsbrowser"
minSdkVersion 23
targetSdkVersion 33
versionCode 1
versionName "0.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
dataBinding = true
viewBinding = true
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
buildToolsVersion '33.0.1'
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'com.github.doyaaaaaken:kotlin-csv-jvm:1.8.0'
implementation 'com.github.wrdlbrnft:sorted-list-adapter:0.3.0.27'
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.github.bumptech.glide:glide:4.14.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
}
+117
View File
@@ -0,0 +1,117 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
kotlin("android")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("keystore.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.illusionware.npsbrowser"
compileSdk = 33
defaultConfig {
applicationId = "com.illusionware.npsbrowser"
minSdk = 23
targetSdk = 33
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildFeatures {
compose = true
aidl = false
renderScript = false
shaders = false
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String?
}
}
buildTypes {
named("release") {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
}
buildToolsVersion = "33.0.1"
kotlinOptions {
jvmTarget = "17"
}
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2022.10.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.core:core-ktx:1.10.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2022.10.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
// Jetpack Compose
val composeBom = platform("androidx.compose:compose-bom:2023.04.01")
implementation(composeBom)
androidTestImplementation(composeBom)
// Material 3
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.compose.material:material-icons-extended:1.4.3")
// Android Studio Preview support
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
// Preferences DataStore (alternative to SharedPreferences)
implementation("androidx.datastore:datastore-preferences:1.0.0")
// Compose extras library for controlling system ui and etc.
implementation("com.google.accompanist:accompanist-systemuicontroller:0.31.2-alpha")
implementation("com.github.IllusionMan1212:kotlin-csv:cedb7c5ab2")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("com.google.android.material:material:1.9.0")
implementation("com.github.bumptech.glide:glide:4.14.2")
annotationProcessor("com.github.bumptech.glide:compiler:4.14.2")
debugImplementation("androidx.compose.ui:ui-test-manifest")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.navigation:navigation-compose:2.6.0")
implementation("io.coil-kt:coil:2.4.0")
implementation("io.coil-kt:coil-compose:2.4.0")
}
+1 -1
View File
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
+10 -7
View File
@@ -7,23 +7,26 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".NPSApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/NPSTheme">
<activity android:name=".activities.SplashActivity" android:theme="@style/NPSTheme.Splash"
android:exported="true">
android:theme="@style/NPSTheme"
android:usesCleartextTraffic="true">
<activity
android:name=".activities.MainActivity"
android:configChanges="screenSize|screenLayout|keyboardHidden|orientation"
android:exported="true"
android:theme="@style/NPSTheme.Splash"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".activities.MainActivity"
android:configChanges="screenSize|screenLayout|keyboardHidden|orientation">
</activity>
</application>
</manifest>
@@ -1,61 +0,0 @@
package com.illusionware.npsbrowser
import com.github.wrdlbrnft.sortedlistadapter.SortedListAdapter
data class AppData(
val titleID: String?, val region: String?, val title: String?, val link: String?,
val license: String?, val contentID: String?, val lastDateTime: String?,
val fileSize: Number?, val version: String?, val sha256: String?, val minFW: String?
) : SortedListAdapter.ViewModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AppData
if (titleID != other.titleID) return false
if (region != other.region) return false
if (title != other.title) return false
if (link != other.link) return false
if (license != other.license) return false
if (contentID != other.contentID) return false
if (lastDateTime != other.lastDateTime) return false
if (fileSize != other.fileSize) return false
if (version != other.version) return false
if (sha256 != other.sha256) return false
if (minFW != other.minFW) return false
return true
}
override fun hashCode(): Int {
var result = titleID?.hashCode() ?: 0
result = 31 * result + (region?.hashCode() ?: 0)
result = 31 * result + (title?.hashCode() ?: 0)
result = 31 * result + (link?.hashCode() ?: 0)
result = 31 * result + (license?.hashCode() ?: 0)
result = 31 * result + (contentID?.hashCode() ?: 0)
result = 31 * result + (lastDateTime?.hashCode() ?: 0)
result = 31 * result + (fileSize?.hashCode() ?: 0)
result = 31 * result + (version?.hashCode() ?: 0)
result = 31 * result + (sha256?.hashCode() ?: 0)
result = 31 * result + (minFW?.hashCode() ?: 0)
return result
}
override fun <T : Any> isSameModelAs(model: T): Boolean {
if (model is AppData) {
val other: AppData = model
return "${other.titleID} ${other.title}" == "$titleID $title"
}
return false
}
override fun <T : Any> isContentTheSameAs(model: T): Boolean {
if (model is AppData) {
val other: AppData = model
return title?.equals(other.title) ?: (other.title == null)
}
return false
}
}
@@ -0,0 +1,25 @@
package com.illusionware.npsbrowser
import android.app.Application
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.illusionware.npsbrowser.data.OnboardingPreferencesRepository
import com.illusionware.npsbrowser.data.SettingsPreferencesRepository
private const val PREFERENCES_NAME = "preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = PREFERENCES_NAME
)
class NPSApp: Application() {
lateinit var settingsPreferencesRepository: SettingsPreferencesRepository
lateinit var onboardingPreferencesRepository: OnboardingPreferencesRepository
override fun onCreate() {
super.onCreate()
settingsPreferencesRepository = SettingsPreferencesRepository(dataStore)
onboardingPreferencesRepository = OnboardingPreferencesRepository(dataStore)
}
}
@@ -1,44 +0,0 @@
package com.illusionware.npsbrowser
import android.content.Context
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener
class RecyclerTouchListener(context: Context?, recyclerView: RecyclerView,
private val clickListener: ClickListener?) : OnItemTouchListener {
interface ClickListener {
fun onClick(view: View?, position: Int)
fun onLongClick(view: View?, recyclerView: RecyclerView?, position: Int)
}
private val gestureDetector: GestureDetector
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
val child = rv.findChildViewUnder(e.x, e.y)
if (child != null && clickListener != null && gestureDetector.onTouchEvent(e)) {
clickListener.onClick(child, rv.getChildAdapterPosition(child))
}
return false
}
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
init {
gestureDetector = GestureDetector(context, object : SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
return true
}
override fun onLongPress(e: MotionEvent) {
val child = recyclerView.findChildViewUnder(e.x, e.y)
if (child != null && clickListener != null) {
clickListener.onLongClick(child, recyclerView, recyclerView.getChildAdapterPosition(child))
}
}
})
}
}
@@ -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")
}
@@ -1,34 +0,0 @@
package com.illusionware.npsbrowser
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.ListPreference
class ThemePreference : ListPreference {
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : super(context!!, attrs, defStyleAttr)
constructor(context : Context?, attrs : AttributeSet?) : super(context!!, attrs)
constructor(context : Context?) : super(context!!)
override fun callChangeListener(newValue : Any?) : Boolean {
AppCompatDelegate.setDefaultNightMode(when ((newValue as String).toInt()) {
0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
})
return super.callChangeListener(newValue)
}
override fun setValue(value: String?) {
super.setValue(value)
summary = value
}
override fun setSummary(summary: CharSequence?) {
super.setSummary(entry)
}
}
@@ -1,40 +0,0 @@
package com.illusionware.npsbrowser.activities
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceManager
import com.illusionware.npsbrowser.R
open class BaseActivity : AppCompatActivity() {
private lateinit var sharedPreferences : SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.root_preferences, false)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val decor: View = window.decorView
AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) {
0 -> {
decor.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
AppCompatDelegate.MODE_NIGHT_NO
}
1 -> {
decor.systemUiVisibility = 0;
AppCompatDelegate.MODE_NIGHT_YES
}
2 -> {
decor.systemUiVisibility = if (resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) 0 else View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
})
}
}
@@ -1,55 +1,711 @@
package com.illusionware.npsbrowser.activities
import android.os.Bundle
import android.view.MenuItem
import androidx.fragment.app.FragmentManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
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.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
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.fragments.mainactivity.MainActivityFragment
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
class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener {
val PlaceholderColor = Color(0x1F888888)
val redHatDisplayFamily = FontFamily(
Font(R.font.red_hat_display_bold, FontWeight.Bold),
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setTheme(R.style.NPSTheme)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
supportFragmentManager.addOnBackStackChangedListener(this);
setContent {
val settingsViewModel: SettingsViewModel = viewModel(
factory = SettingsViewModel.Factory
)
val onboardingViewModel: OnboardingViewModel = viewModel(
factory = OnboardingViewModel.Factory
)
val packageDetailsViewModel: PackageDetailsViewModel = viewModel()
if (supportFragmentManager.backStackEntryCount > 0) {
showUpButton()
val onboardingPrefs = onboardingViewModel.uiState.collectAsStateWithLifecycle().value
val appTheme = runBlocking { settingsViewModel.getAppTheme() }
val settingsPrefs = settingsViewModel.uiState.collectAsStateWithLifecycle().value
val navController = rememberNavController()
val darkTheme = when (appTheme) {
0 -> false
1 -> true
else -> isSystemInDarkTheme()
}
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainActivityFragment())
.commitNow()
NPSBrowserTheme(darkTheme = darkTheme) {
Surface(
modifier = Modifier.fillMaxSize(),
color = if (onboardingPrefs.seenOnboarding) MaterialTheme.colorScheme.background else ColorAccent
) {
NavHost(
navController = navController,
startDestination = if (onboardingPrefs.seenOnboarding) Routes.Home.route else Routes.Onboarding.route,
) {
composable(Routes.Home.route) {
HomePage(
navigateToSettings = { navController.navigate(Routes.Settings.route) },
settingsPrefs = settingsPrefs,
settingsViewModel = settingsViewModel,
navigateToDetailsScreen = { item ->
packageDetailsViewModel.setSelectedPackage(item)
navController.navigate(Routes.PackageDetails.route)
},
)
}
composable(Routes.Settings.route) {
SettingsScreen(
navigationGoBack = { navController.popBackStack() },
)
}
composable(Routes.Onboarding.route) {
OnBoarding(
navigateToHome = {
navController.popBackStack()
navController.navigate(Routes.Home.route)
},
darkTheme = darkTheme,
)
}
composable(Routes.PackageDetails.route) {
PackageDetails(
navigationGoBack = { navController.popBackStack() },
packageDetailsViewModel = packageDetailsViewModel,
)
}
}
}
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
supportFragmentManager.popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomePage(
navigateToSettings: () -> Unit,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
settingsPrefs: SettingsPreferences,
settingsViewModel: SettingsViewModel,
) {
var isSearchOpen by rememberSaveable { mutableStateOf(false)}
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)
override fun onBackStackChanged() {
// enable Up button only if there are entries on the backstack
if (supportFragmentManager.backStackEntryCount < 1) {
hideUpButton()
val tsvs = settingsViewModel.availableTsvs()
var selectedBarItem by rememberSaveable { mutableStateOf(ConsoleType.PSVITA) }
LaunchedEffect(systemUiController) {
systemUiController.setNavigationBarColor(
color = if (context.isNavigationBarNeedsScrim()) {
navbarScrimColor.copy(alpha = 0.7f)
} else {
showUpButton()
Color.Transparent
}
)
}
Scaffold(
topBar = {
TopAppBar(
title = {
if (!isSearchOpen) {
Text(text = stringResource(id = R.string.app_name))
}
},
actions = {
if (isSearchOpen) {
NPSIconButton(
tooltip = "Close Search",
onClick = { isSearchOpen = false }
){
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Close Search")
}
Spacer(Modifier.width(12.dp))
TextField(
value = searchQuery,
onValueChange = {str -> searchQuery = str},
Modifier
.weight(1.0f)
.focusRequester(searchBar),
placeholder = {
Text(text = stringResource(id = R.string.search), style = Typography.bodyLarge)
},
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.primary,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
)
Spacer(Modifier.width(12.dp))
if (searchQuery.isNotEmpty()) {
NPSIconButton(
tooltip = "Clear Search",
onClick = { searchQuery = "" }
){
Icon(imageVector = Icons.Filled.Close, contentDescription = "Clear Search")
}
}
private fun showUpButton() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
LaunchedEffect(Unit) {
searchBar.requestFocus()
}
}
if (!isSearchOpen) {
NPSIconButton(
tooltip = stringResource(id = R.string.search_tooltip),
onClick = { isSearchOpen = true }
){
Icon(imageVector = Icons.Filled.Search, contentDescription = stringResource(
id = R.string.search_tooltip
))
}
}
NPSIconButton(
tooltip = stringResource(id = R.string.layout),
onClick = { layoutDialogOpen = true }
) {
Icon(imageVector = if (settingsPrefs.layout == ItemLayout.GRID.ordinal) Icons.Filled.GridView else Icons.Filled.ViewList, contentDescription = stringResource(
id = R.string.layout
))
}
NPSIconButton(
tooltip = stringResource(id = R.string.title_activity_settings),
onClick = { navigateToSettings() }
){
Icon(imageVector = Icons.Filled.Settings, contentDescription = stringResource(
id = R.string.title_activity_settings
))
}
}
)
},
bottomBar = {
if (tsvs.isNotEmpty()) {
NavBar(
selectedTab = selectedBarItem,
onTabSelected = { selectedBarItem = it },
)
}
}
) { padding ->
Column(Modifier.padding(padding)) {
if (tsvs.isEmpty()) {
NoTsvs(navigateToSettings = navigateToSettings)
} else {
PackageList(
navigateToDetailsScreen = navigateToDetailsScreen,
type = selectedBarItem,
settingsPrefs = settingsPrefs
)
}
}
private fun hideUpButton() {
supportActionBar!!.setDisplayHomeAsUpEnabled(false)
if (layoutDialogOpen) {
LayoutDialog(
onDismiss = { layoutDialogOpen = false },
prefs = settingsPrefs,
viewModel = settingsViewModel,
)
}
}
}
@Composable
fun NavBar(selectedTab: ConsoleType, onTabSelected: (ConsoleType) -> Unit) {
NavigationBar {
NavigationBarItem(
selected = selectedTab == ConsoleType.PSVITA,
icon = { Icon(painterResource(id = R.drawable.vita_icon), contentDescription = "PS Vita") },
label = { Text(text = "PS Vita") },
onClick = {
onTabSelected(ConsoleType.PSVITA)
}
)
NavigationBarItem(
selected = selectedTab == ConsoleType.PS3,
icon = { Icon(painterResource(id = R.drawable.ps3_icon), contentDescription = "PS3") },
label = { Text(text = "PS3") },
onClick = {
onTabSelected(ConsoleType.PS3)
}
)
NavigationBarItem(
selected = selectedTab == ConsoleType.PSP,
icon = { Icon(painterResource(id = R.drawable.psp_icon), contentDescription = "PSP") },
label = { Text(text = "PSP") },
onClick = {
onTabSelected(ConsoleType.PSP)
}
)
NavigationBarItem(
selected = selectedTab == ConsoleType.PSX,
icon = { Icon(painterResource(id = R.drawable.psx_icon), contentDescription = "PSX") },
label = { Text(text = "PSX") },
onClick = {
onTabSelected(ConsoleType.PSX)
}
)
NavigationBarItem(
selected = selectedTab == ConsoleType.PSM,
icon = { Icon(painterResource(id = R.drawable.psm_icon), contentDescription = "PSM") },
label = { Text(text = "PSM") },
onClick = {
onTabSelected(ConsoleType.PSM)
}
)
}
}
@Composable
@Preview
fun NoTsvs(padding: PaddingValues = PaddingValues(0.dp), navigateToSettings: () -> Unit = {}) {
Column(
Modifier
.fillMaxSize()
.padding(padding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(id = R.drawable.ph_info),
contentDescription = "Info",
tint = MaterialTheme.colorScheme.outline,
modifier = Modifier.size(92.dp)
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "You haven't added any TSVs",
color = MaterialTheme.colorScheme.outline,
style = Typography.bodyLarge
)
Row {
Text(
text = "Add your TSV files from the ",
color = MaterialTheme.colorScheme.outline,
style = Typography.bodyLarge
)
Text(
text = "settings",
color = ColorAccent,
style = Typography.bodyLarge,
modifier = Modifier.clickable {
navigateToSettings()
}
)
}
}
}
@Composable
fun PackageList(
type: ConsoleType = ConsoleType.PSVITA,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
viewModel: PackageListViewModel = viewModel(
factory = PackageListViewModel.Factory
),
settingsPrefs: SettingsPreferences,
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
val packages = when (type) {
ConsoleType.PSVITA -> {
uiState.packages.filter { it.consoleType == ConsoleType.PSVITA }
}
ConsoleType.PS3 -> {
uiState.packages.filter { it.consoleType == ConsoleType.PS3 }
}
ConsoleType.PSP -> {
uiState.packages.filter { it.consoleType == ConsoleType.PSP }
}
ConsoleType.PSX -> {
uiState.packages.filter { it.consoleType == ConsoleType.PSX }
}
ConsoleType.PSM -> {
uiState.packages.filter { it.consoleType == ConsoleType.PSM }
}
}
Column(Modifier.fillMaxSize()) {
if (uiState.isLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
if (packages.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
"You haven't added any TSVs for this console yet",
style = Typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.outline
)
}
} else {
if (settingsPrefs.layout == ItemLayout.LIST.ordinal) {
LazyColumn {
items(packages) {
PackageListItem(it, navigateToDetailsScreen)
}
}
} else {
LazyVerticalGrid(
columns = GridCells.Adaptive(140.dp),
contentPadding = PaddingValues(8.dp),
) {
items(packages) {
PackageGridItem(it, navigateToDetailsScreen)
}
}
}
}
}
}
}
@Composable
fun PackageListItem(
item: PackageItem,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
) {
Box(
Modifier
.fillMaxWidth()
.clickable { navigateToDetailsScreen(item) }
) {
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.weight(1.0f)
) {
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.size(48.dp),
contentScale = ContentScale.Crop,
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = item.name,
color = MaterialTheme.colorScheme.onBackground,
style = Typography.bodyLarge,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
modifier = Modifier
.width(IntrinsicSize.Min)
.weight(1.0f)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
Text(text = item.region)
if (!item.minFW.isNullOrEmpty()) {
Text(text = item.minFW)
}
if (item.pkgSize.isNotEmpty()) {
Text(text = item.pkgSize)
}
Tag(title = item.dataType.name, small = true)
}
}
}
}
Column {
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.isNullOrEmpty() && item.rap.isNullOrEmpty())) {
Icon(
painter = painterResource(id = R.drawable.no_key_icon),
contentDescription = "No License Key",
tint = Color(0xFFCB5A5A)
)
}
}
}
}
}
@Composable
fun Tag(title: String, small: Boolean = false) {
Box(
Modifier
.clip(RoundedCornerShape(4.dp))
.background(Color(0xFFEAF654))
.layoutId("tag")
.padding(horizontal = if (small) 6.dp else 8.dp, vertical = if (small) 2.dp else 4.dp)
) {
Text(
text = title.uppercase(),
color = ColorOnPrimaryLight,
style = Typography.bodySmall.copy(
fontWeight = FontWeight.Bold,
fontFamily = redHatDisplayFamily,
fontSize = if (small) 8.sp else 12.sp
)
)
}
}
@Composable
fun PackageGridItem(
item: PackageItem,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
) {
Box(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { navigateToDetailsScreen(item) },
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.Start,
modifier = Modifier.padding(8.dp),
) {
Layout(
content = {
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,
)
Tag(title = item.dataType.name)
}
) { measurables, constraints ->
val imagePlaceable = measurables[0].measure(constraints)
val tagPlaceable = measurables.first { it.layoutId == "tag" }.measure(constraints)
layout(imagePlaceable.width, imagePlaceable.height) {
imagePlaceable.placeRelative(0, 0)
tagPlaceable.placeRelative(
x = imagePlaceable.width - tagPlaceable.width - 16,
y = 16,
)
}
}
Text(
text = item.name,
color = MaterialTheme.colorScheme.onBackground,
style = Typography.bodyLarge,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
Text(text = item.region)
if (!item.minFW.isNullOrEmpty()) {
Text(text = item.minFW)
}
if (item.pkgSize.isNotEmpty()) {
Text(text = item.pkgSize)
}
}
}
Row {
if (item.pkgUrl.isNullOrEmpty()) {
Icon(
painter = painterResource(id = R.drawable.no_url_icon),
contentDescription = "No PKG URL",
tint = Color(0xFFCB5A5A)
)
}
if (item.zRif.isNullOrEmpty() && item.rap.isNullOrEmpty()) {
Icon(
painter = painterResource(id = R.drawable.no_key_icon),
contentDescription = "No License Key",
tint = Color(0xFFCB5A5A)
)
}
}
}
}
}
@Composable
fun LayoutDialog(
onDismiss: () -> Unit,
prefs: SettingsPreferences,
viewModel: SettingsViewModel
) {
NPSAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.layout),
buttons = {
TextButton(onClick = onDismiss) {
Text(text = "Cancel")
}
}
) {
Column {
NPSRadioButton(
text = "List",
selected = prefs.layout == ItemLayout.LIST.ordinal,
onClick = { viewModel.setLayout(ItemLayout.LIST) },
)
NPSRadioButton(
text = "Grid",
selected = prefs.layout == ItemLayout.GRID.ordinal,
onClick = { viewModel.setLayout(ItemLayout.GRID) },
)
}
}
}
@@ -1,35 +0,0 @@
package com.illusionware.npsbrowser.activities
import android.content.Intent
import android.os.Bundle
import android.view.View
class SplashActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(Intent(this, MainActivity::class.java))
finish()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) hideSystemUI()
}
private fun hideSystemUI() {
// Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
}
@@ -1,86 +0,0 @@
package com.illusionware.npsbrowser.adapters
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import com.github.wrdlbrnft.modularadapter.ModularAdapter
import com.github.wrdlbrnft.sortedlistadapter.SortedListAdapter
import com.illusionware.npsbrowser.AppData
import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.activities.MainActivity
import com.illusionware.npsbrowser.fragments.SingleAppFragment
import com.illusionware.npsbrowser.fragments.mainactivity.MainActivityFragment
import java.util.Comparator
private val comparator: Comparator<AppData> = SortedListAdapter.ComparatorBuilder<AppData>()
.setOrderForModel(AppData::class.java) { a, b -> a.title?.compareTo(b.title!!)!! }
.build()
class AppAdapter(val context: Context) : SortedListAdapter<AppData>(context, AppData::class.java, comparator) {
private val LIST_ITEM = 0
private val GRID_ITEM = 1
private var isSwitchView = true
inner class AppViewHolder(itemView: View) :
SortedListAdapter.ViewHolder<AppData?>(itemView) {
var appTitle: TextView = itemView.findViewById(R.id.appTitle)
var appRegion: TextView = itemView.findViewById(R.id.appRegion)
var appMinFW: TextView = itemView.findViewById(R.id.appMinFW)
var appLastDate: TextView = itemView.findViewById(R.id.appLastDate)
override fun performBind(item: AppData) {
appTitle.text = item.titleID + " | " + item.title
appTitle.isSelected = true
appRegion.text = item.region
appMinFW.text = item.minFW
appMinFW.background = null;
if (appMinFW.text.isNotBlank()) {
val drawable = ResourcesCompat.getDrawable(
context.resources,
R.drawable.rect_red,
null
)
appMinFW.background = drawable;
}
appLastDate.text = item.lastDateTime
}
}
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): AppViewHolder {
return if (viewType == LIST_ITEM) {
AppViewHolder(
inflater.inflate(
R.layout.recycler_item_list,
parent,
false
)
)
} else {
AppViewHolder(
inflater.inflate(
R.layout.recycler_item_grid,
parent,
false
)
)
}
}
override fun getItemViewType(position: Int): Int {
return if (isSwitchView) {
LIST_ITEM
} else {
GRID_ITEM
}
}
fun toggleItemViewType(type: Int): Boolean {
isSwitchView = type == 0
return isSwitchView
}
}
@@ -0,0 +1,36 @@
package com.illusionware.npsbrowser.data
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
data class OnboardingPreferences(
val seenOnboarding: Boolean,
)
class OnboardingPreferencesRepository(
private val dataStore: DataStore<Preferences>,
) {
companion object {
private val SEEN_ONBOARDING = booleanPreferencesKey("seen_onboarding")
}
fun getPreferences(): Flow<OnboardingPreferences> =
runBlocking { dataStore.data.map { mapOnboardingSettings(it) } }
suspend fun setSeenOnboarding(seen: Boolean) {
dataStore.edit { prefs ->
prefs[SEEN_ONBOARDING] = seen
}
}
private fun mapOnboardingSettings(prefs: Preferences): OnboardingPreferences {
return OnboardingPreferences(
prefs[SEEN_ONBOARDING] ?: false,
)
}
}
@@ -0,0 +1,219 @@
package com.illusionware.npsbrowser.data
import android.net.Uri
import android.os.Environment
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import com.illusionware.npsbrowser.model.ConsoleType
import com.illusionware.npsbrowser.model.PackageItemType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
val defaultDownloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/NPSBrowser"
enum class ItemLayout {
LIST,
GRID,
}
enum class Theme {
LIGHT,
DARK,
SYSTEM,
}
data class SettingsPreferences(
val appTheme: Int,
val layout: Int,
val psvGames: String,
val ps3Games: String,
val pspGames: String,
val psxGames: String,
val psmGames: String,
val psvDlc: String,
val ps3Dlc: String,
val pspDlc: String,
val psvThemes: String,
val hmacKey: String,
val downloadDir: String,
val unpackDir: String,
val unpackInDownload: Boolean,
val deleteAfterUnpack: Boolean,
val pkg2zipParams: String,
val pkg2zipAutoDecrypt: Boolean,
)
class SettingsPreferencesRepository(
private val dataStore: DataStore<Preferences>
) {
companion object {
private val APP_THEME = intPreferencesKey("app_theme")
private val LAYOUT = intPreferencesKey("layout")
private val PSV_GAMES = stringPreferencesKey("psv_games")
private val PS3_GAMES = stringPreferencesKey("ps3_games")
private val PSP_GAMES = stringPreferencesKey("psp_games")
private val PSX_GAMES = stringPreferencesKey("psx_games")
private val PSM_GAMES = stringPreferencesKey("psm_games")
private val PSV_DLC = stringPreferencesKey("psv_dlc")
private val PS3_DLC = stringPreferencesKey("ps3_dlc")
private val PSP_DLC = stringPreferencesKey("psp_dlc")
private val PSV_THEMES = stringPreferencesKey("psv_themes")
private val HMAC_KEY = stringPreferencesKey("hmac_key")
private val DOWNLOAD_DIR = stringPreferencesKey("download_dir")
private val UNPACK_DIR = stringPreferencesKey("unpack_dir")
private val UNPACK_IN_DOWNLOAD = booleanPreferencesKey("unpack_in_download")
private val DELETE_AFTER_UNPACK = booleanPreferencesKey("delete_after_unpack")
private val PKG2ZIP_PARAMS = stringPreferencesKey("pkg2zip_params")
private val PKG2ZIP_AUTO_DECRYPT = booleanPreferencesKey("pkg2zip_auto_decrypt")
}
private val tsvFiles = MutableSharedFlow<Triple<String, ConsoleType, PackageItemType>>()
fun getPreferences(): Flow<SettingsPreferences> =
dataStore.data.map { mapSettingsPreferences(it) }
suspend fun availableTsvs(): MutableList<Triple<String, ConsoleType, PackageItemType>> {
val list = mutableListOf<Triple<String, ConsoleType, PackageItemType>>()
return dataStore.data.map {
if (it[PSV_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSV_GAMES] ?: "", ConsoleType.PSVITA, PackageItemType.GAME))
if (it[PSV_DLC]?.isNotEmpty() == true) list.add(Triple(it[PSV_DLC] ?: "", ConsoleType.PSVITA, PackageItemType.DLC))
if (it[PSV_THEMES]?.isNotEmpty() == true) list.add(Triple(it[PSV_THEMES] ?: "", ConsoleType.PSVITA, PackageItemType.THEME))
if (it[PS3_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PS3_GAMES] ?: "", ConsoleType.PS3, PackageItemType.GAME))
if (it[PS3_DLC]?.isNotEmpty() == true) list.add(Triple(it[PS3_DLC] ?: "", ConsoleType.PS3, PackageItemType.DLC))
if (it[PSP_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSP_GAMES] ?: "", ConsoleType.PSP, PackageItemType.GAME))
if (it[PSP_DLC]?.isNotEmpty() == true) list.add(Triple(it[PSP_DLC] ?: "", ConsoleType.PSP, PackageItemType.DLC))
if (it[PSX_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSX_GAMES] ?: "", ConsoleType.PSX, PackageItemType.GAME))
if (it[PSM_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSM_GAMES] ?: "", ConsoleType.PSM, PackageItemType.GAME))
list
}.first()
}
suspend fun setTheme(theme: Theme) {
dataStore.edit { prefs ->
prefs[APP_THEME] = theme.ordinal
}
}
suspend fun setLayout(layout: ItemLayout) {
dataStore.edit { prefs ->
prefs[LAYOUT] = layout.ordinal
}
}
suspend fun setHMACKey(key: String) {
dataStore.edit { prefs ->
prefs[HMAC_KEY] = key
}
}
suspend fun setDownloadDir(dir: String) {
dataStore.edit { prefs ->
prefs[DOWNLOAD_DIR] = dir
}
}
suspend fun setUnpackDir(dir: String) {
dataStore.edit { prefs ->
prefs[UNPACK_DIR] = dir
}
}
suspend fun setUnpackInDownload(unpackInDownload: Boolean) {
dataStore.edit { prefs ->
prefs[UNPACK_IN_DOWNLOAD] = unpackInDownload
}
}
suspend fun setDeleteAfterUnpack(deleteAfterUnpack: Boolean) {
dataStore.edit { prefs ->
prefs[DELETE_AFTER_UNPACK] = deleteAfterUnpack
}
}
suspend fun setPkg2zipParams(params: String) {
dataStore.edit { prefs ->
prefs[PKG2ZIP_PARAMS] = params
}
}
suspend fun setPkg2zipAutoDecrypt(autoDecrypt: Boolean) {
dataStore.edit { prefs ->
prefs[PKG2ZIP_AUTO_DECRYPT] = autoDecrypt
}
}
fun tsvFiles() = tsvFiles
suspend fun setTSVFile(console: ConsoleType, contentType: PackageItemType, file: Uri) {
tsvFiles.emit(Triple(file.toString(), console, contentType))
dataStore.edit { prefs ->
when (console) {
ConsoleType.PSVITA -> {
when (contentType) {
PackageItemType.GAME -> prefs[PSV_GAMES] = file.toString()
PackageItemType.DLC -> prefs[PSV_DLC] = file.toString()
PackageItemType.THEME -> prefs[PSV_THEMES] = file.toString()
}
}
ConsoleType.PS3 -> {
when (contentType) {
PackageItemType.GAME -> prefs[PS3_GAMES] = file.toString()
PackageItemType.DLC -> prefs[PS3_DLC] = file.toString()
else -> {}
}
}
ConsoleType.PSP -> {
when (contentType) {
PackageItemType.GAME -> prefs[PSP_GAMES] = file.toString()
PackageItemType.DLC -> prefs[PSP_DLC] = file.toString()
else -> {}
}
}
ConsoleType.PSX -> {
when (contentType) {
PackageItemType.GAME -> prefs[PSX_GAMES] = file.toString()
else -> {}
}
}
ConsoleType.PSM -> {
when (contentType) {
PackageItemType.GAME -> prefs[PSM_GAMES] = file.toString()
else -> {}
}
}
}
}
}
private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences {
return SettingsPreferences(
prefs[APP_THEME] ?: Theme.SYSTEM.ordinal,
prefs[LAYOUT] ?: ItemLayout.LIST.ordinal,
prefs[PSV_GAMES] ?: "",
prefs[PS3_GAMES] ?: "",
prefs[PSP_GAMES] ?: "",
prefs[PSX_GAMES] ?: "",
prefs[PSM_GAMES] ?: "",
prefs[PSV_DLC] ?: "",
prefs[PS3_DLC] ?: "",
prefs[PSP_DLC] ?: "",
prefs[PSV_THEMES] ?: "",
prefs[HMAC_KEY] ?: "",
prefs[DOWNLOAD_DIR] ?: defaultDownloadDir,
prefs[UNPACK_DIR] ?: defaultDownloadDir,
prefs[UNPACK_IN_DOWNLOAD] ?: false,
prefs[DELETE_AFTER_UNPACK] ?: false,
prefs[PKG2ZIP_PARAMS] ?: "-x {pkgFile} \"{zRifKey}\"",
prefs[PKG2ZIP_AUTO_DECRYPT] ?: true,
)
}
}
@@ -1,42 +0,0 @@
package com.illusionware.npsbrowser.fragments
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.illusionware.npsbrowser.R
class SettingsFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(requireContext(), R.xml.root_preferences, false)
activity?.supportFragmentManager
?.beginTransaction()
?.replace(
R.id.prefs,
PrefsFragment()
)
?.setTransition(TRANSIT_FRAGMENT_OPEN)
?.commit()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
activity?.title = resources.getString(R.string.title_activity_settings)
return inflater.inflate(R.layout.settings_fragment, container, false)
}
class PrefsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
PreferenceManager.setDefaultValues(requireContext(), R.xml.root_preferences, false)
}
}
}
@@ -1,193 +0,0 @@
package com.illusionware.npsbrowser.fragments
import android.app.DownloadManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import android.content.Context.DOWNLOAD_SERVICE
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.bumptech.glide.Glide
import com.illusionware.npsbrowser.AppData
import com.illusionware.npsbrowser.R
class SingleAppFragment : Fragment() {
private val baseStoreURL = "https://store.playstation.com/store/api/chihiro/00_09_000/container"
companion object {
lateinit var app : AppData
}
// TODO: either use binding or make vars for all the views
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
activity?.title = app.title
val view = inflater.inflate(R.layout.single_app_fragment, container, false)
val downloadManager = requireActivity().getSystemService(DOWNLOAD_SERVICE) as DownloadManager
view?.findViewById<TextView>(R.id.singleAppTitle)?.text = "[${app.titleID}] ${app.title}"
if (app.fileSize != null) {
view?.findViewById<TextView>(R.id.singleAppSize)?.text = prettifySize()
} else {
view?.findViewById<TextView>(R.id.singleAppSizeTitle)?.visibility = View.INVISIBLE
}
view?.findViewById<TextView>(R.id.singleAppContentId)?.text = app.contentID
if (app.version != null) {
view?.findViewById<TextView>(R.id.singleAppVer)?.text = app.version
} else {
view?.findViewById<TextView>(R.id.singleAppVerTitle)?.visibility = View.INVISIBLE
}
view?.findViewById<TextView>(R.id.singleAppFwVer)?.text = app.minFW
// TODO: get and set app icon
if (app.minFW.isNullOrBlank()) {
view?.findViewById<TextView>(R.id.singleAppFwVerTitle)?.visibility = View.INVISIBLE
}
if (app.license == "MISSING") {
view?.findViewById<TextView>(R.id.singleAppError)?.text = getString(R.string.no_license)
view?.findViewById<Button>(R.id.singleAppCopyZRIFButton)?.isEnabled = false
}
if (app.link == "CART ONLY") {
view?.findViewById<TextView>(R.id.singleAppError)?.text = getString(R.string.cart_only)
view?.findViewById<Button>(R.id.singleAppDownloadButton)?.isEnabled = false
view?.findViewById<Button>(R.id.singleAppCopyPKGButton)?.isEnabled = false
view?.findViewById<Button>(R.id.singleAppCopyZRIFButton)?.isEnabled = false
} else if (app.link == "MISSING") {
view?.findViewById<TextView>(R.id.singleAppError)?.text = getString(R.string.no_download_link)
view?.findViewById<Button>(R.id.singleAppDownloadButton)?.isEnabled = false
view?.findViewById<Button>(R.id.singleAppCopyPKGButton)?.isEnabled = false
view?.findViewById<Button>(R.id.singleAppCopyZRIFButton)?.isEnabled = false
}
view?.findViewById<TextView>(R.id.singleAppContentId)?.setOnLongClickListener {
val clipboardManager = requireActivity().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText(
"Content ID",
view.findViewById<TextView>(R.id.singleAppContentId)?.text
)
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(requireContext(), getString(R.string.content_id_copied), Toast.LENGTH_SHORT).show()
true
}
// click listener for the download button
view?.findViewById<Button>(R.id.singleAppDownloadButton)?.setOnClickListener {
try {
val request = DownloadManager.Request(Uri.parse(app.link))
request.setTitle(app.title)
request.setDescription(getString(R.string.downloading))
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
app.title!!.replace("[\\\\/:*?\"<>|]".toRegex(), " ") + ".pkg"
) // TODO: set up preference for this
downloadManager.enqueue(request)
} catch (e: Exception) {
Toast.makeText(requireContext(), getString(R.string.download_error), Toast.LENGTH_SHORT).show()
}
}
// click listener for the copy pkg button
view?.findViewById<Button>(R.id.singleAppCopyPKGButton)?.setOnClickListener {
val clipboardManager = requireActivity().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("PKG URL", app.link)
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(requireContext(),getString(R.string.pkg_url_copied),Toast.LENGTH_SHORT).show()
}
// click listener for the copy zRIF button
view?.findViewById<Button>(R.id.singleAppCopyZRIFButton)?.setOnClickListener {
val clipboardManager = requireActivity().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("zRIF", app.license)
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(requireContext(),getString(R.string.zrif_copied),Toast.LENGTH_SHORT).show()
}
// set the height for the error text to 0 if there's no error so that the scrollview can take more space
if (view?.findViewById<TextView>(R.id.singleAppError)?.text.isNullOrEmpty()) {
view?.findViewById<TextView>(R.id.singleAppError)?.layoutParams?.height = 0
}
if (app.contentID != null) {
val circularProgressDrawable = CircularProgressDrawable(requireContext())
circularProgressDrawable.setColorSchemeColors(0x43EB92)
circularProgressDrawable.strokeWidth = 10f
circularProgressDrawable.centerRadius = 180f
circularProgressDrawable.start()
val image : ImageView? = view?.findViewById(R.id.singleAppIcon)
val iconURL = getImage(app.contentID!!)
if (iconURL != null) {
// TODO: make the options a requestOptions
Glide.with(this).load(iconURL).placeholder(circularProgressDrawable).error(R.drawable.ic_games_placeholder_24dp).into(image!!)
}
}
return view
}
private fun prettifySize(): String {
if (app.fileSize!!.toFloat() / (1024 * 1024 * 1024) > 1) {
return "%.1f GB".format(toGB(app.fileSize!!))
}
if (app.fileSize!!.toFloat() / (1024 * 1024) > 1) {
return "%.1f MB".format(toMB(app.fileSize!!))
}
if (app.fileSize!!.toFloat() / 1024 > 1) {
return "%.1f KB".format(toKB(app.fileSize!!))
}
return "${app.fileSize!!} Bytes"
}
private fun toKB(size: Number): Number {
return size.toFloat() / 1024
}
private fun toMB(size: Number): Number {
return size.toFloat() / (1024 * 1024)
}
private fun toGB(size: Number): Number {
return size.toFloat() / (1024 * 1024 * 1024)
}
private fun getImage(contentID: String): String? {
when (contentID.substring(0, 2)) {
"EP" -> {
// european games
return "$baseStoreURL/SA/en/999/${contentID}/image"
}
"UP" -> {
// united states games
return "$baseStoreURL/US/en/999/${contentID}/image"
}
"JP" -> {
// japanese games
return "$baseStoreURL/jp/ja/999/${contentID}/image"
}
"KP" -> {
// korean games
return "$baseStoreURL/kr/ko/999/${contentID}/image"
}
"HP" -> {
// asia games
return "$baseStoreURL/HK/zh/999/${contentID}/image"
}
}
return null
}
}
@@ -1,284 +0,0 @@
package com.illusionware.npsbrowser.fragments.mainactivity
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import android.view.*
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.illusionware.npsbrowser.AppData
import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.RecyclerTouchListener
import com.illusionware.npsbrowser.fragments.SingleAppFragment
import com.illusionware.npsbrowser.adapters.AppAdapter
import com.illusionware.npsbrowser.fragments.SettingsFragment
import java.io.InputStream
import java.util.*
import kotlin.collections.ArrayList
class MainActivityFragment : Fragment() {
var recyclerView : RecyclerView? = null
var viewAdapter: AppAdapter? = null
var filteredModelList: ArrayList<AppData>? = null
companion object {
var apps : ArrayList<AppData>? = arrayListOf()
}
private val tsvReader = csvReader {
charset = "UTF-8"
quoteChar = '\\'
escapeChar = '\\'
delimiter = '\t'
skipEmptyLine = true
skipMissMatchedRow = true
}
private lateinit var viewModel: MainActivityViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setHasOptionsMenu(true)
activity?.title = resources.getString(R.string.app_name)
val view = inflater.inflate(R.layout.main_fragment, container, false)
view?.findViewById<FloatingActionButton>(R.id.tsvChooserButton)?.setOnClickListener {
if (!isStoragePermissionGranted())
return@setOnClickListener
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/tab-separated-values"
}
startActivityForResult(intent, 1)
}
viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
// TODO: Use the ViewModel
recyclerView = view?.findViewById(R.id.appsRecycler)
if (viewAdapter == null) {
viewAdapter = AppAdapter(requireContext())
viewAdapter?.edit()?.add(apps!!)?.commit()
}
if (apps != null) {
filteredModelList = apps
}
viewAdapter?.notifyDataSetChanged()
recyclerView?.adapter = viewAdapter
val orientation = resources.configuration.orientation
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isViewAsList = prefs.getString("layout_type", "0")
if (isViewAsList == "0") {
viewAdapter?.toggleItemViewType(0)
recyclerView?.layoutManager = LinearLayoutManager(context)
viewAdapter?.notifyDataSetChanged()
} else {
viewAdapter?.toggleItemViewType(1)
recyclerView?.layoutManager = GridLayoutManager(
context,
if (orientation == Configuration.ORIENTATION_LANDSCAPE) 4 else 2
)
viewAdapter?.notifyDataSetChanged()
}
recyclerView?.addOnItemTouchListener(RecyclerTouchListener(
requireContext(),
recyclerView!!,
object : RecyclerTouchListener.ClickListener {
override fun onClick(view: View?, position: Int) {
SingleAppFragment.app = filteredModelList!![position]
activity?.supportFragmentManager
?.beginTransaction()
?.replace(R.id.container,
SingleAppFragment()
)
?.addToBackStack("single app")
?.commit()
}
override fun onLongClick(
view: View?,
recyclerView: RecyclerView?,
position: Int
) {
Toast.makeText(context, "TODO: Long Click $position", Toast.LENGTH_SHORT).show()
}
}
))
return view
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
resultData?.data?.also { uri ->
val inputStream: InputStream? = activity?.contentResolver?.openInputStream(uri)
val entries : List<Map<String, String?>> = tsvReader.readAllWithHeader(inputStream!!)
apps?.clear()
apps = ArrayList(entries.size)
entries.forEach { entry ->
if (entry["Name"].isNullOrEmpty()) {
return@forEach
}
val id = entry["Title ID"]
val region = entry["Region"]
val name = entry["Name"]
val link = entry["PKG direct link"]
val license = entry["zRIF"]
val contentID = entry["Content ID"]
var lastDateTime = entry["Last Modification Date"]
if (!lastDateTime.isNullOrEmpty()) {
lastDateTime = lastDateTime.substring(0, 10)
}
val fileSize = entry["File Size"]?.toLongOrNull()
val version = entry["Update Version"]
val sha256 = entry["SHA256"]
val minFW = entry["Required FW"]
val app = AppData(
id, region, name, link ?: "MISSING", license ?: "MISSING",
contentID ?: "MISSING", lastDateTime ?: "MISSING",
fileSize, version, sha256 ?: "MISSING", minFW
)
apps?.add(app)
}
apps?.sortBy { it.title }
filteredModelList = apps
recyclerView = view?.findViewById(R.id.appsRecycler)
viewAdapter = AppAdapter(requireContext())
viewAdapter?.edit()?.add(apps!!)?.commit()
val orientation = this.resources.configuration.orientation
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isViewAsList = prefs.getString("layout_type", "0")
if (isViewAsList == "0") {
viewAdapter?.toggleItemViewType(0)
recyclerView?.layoutManager = LinearLayoutManager(context)
viewAdapter?.notifyDataSetChanged()
} else {
viewAdapter?.toggleItemViewType(1)
recyclerView?.layoutManager = GridLayoutManager(
context,
if (orientation == Configuration.ORIENTATION_LANDSCAPE) 4 else 2
)
viewAdapter?.notifyDataSetChanged()
}
recyclerView?.adapter = viewAdapter
}
}
}
override fun onCreateOptionsMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.main, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView: SearchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(query: String): Boolean {
filteredModelList = filter(apps!!, query) as ArrayList<AppData>?
viewAdapter?.edit()?.replaceAll(filteredModelList!!)?.commit()
recyclerView?.scrollToPosition(0)
return false
}
})
super.onCreateOptionsMenu(menu, menuInflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> {
activity?.supportFragmentManager
?.beginTransaction()
?.replace(
R.id.container,
SettingsFragment()
)
?.addToBackStack("settings")
?.commit()
true
}
R.id.action_search -> {
true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onPause() {
super.onPause()
viewAdapter?.edit()?.replaceAll(apps!!)?.commit()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val orientation = newConfig.orientation
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isViewAsList = prefs.getString("layout_type", "0")
if (isViewAsList == "0") {
val pos = (recyclerView?.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
viewAdapter?.toggleItemViewType(0)
recyclerView?.layoutManager = LinearLayoutManager(context)
(recyclerView?.layoutManager as LinearLayoutManager).scrollToPosition(pos)
viewAdapter?.notifyDataSetChanged()
} else {
val pos = (recyclerView?.layoutManager as GridLayoutManager).findFirstVisibleItemPosition()
viewAdapter?.toggleItemViewType(1)
recyclerView?.layoutManager = GridLayoutManager(
context,
if (orientation == Configuration.ORIENTATION_LANDSCAPE) 4 else 2
)
(recyclerView?.layoutManager as GridLayoutManager).scrollToPosition(pos)
viewAdapter?.notifyDataSetChanged()
}
}
private fun isStoragePermissionGranted(): Boolean {
return if (checkSelfPermission(
this.requireContext(),
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED) {
true
} else {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1)
false
}
}
private fun filter(models: List<AppData>, query: String): List<AppData>? {
val lowerCaseQuery = query.lowercase(Locale.getDefault())
val filteredModelList: MutableList<AppData> = ArrayList()
for (model in models) {
val title: String = model.title?.lowercase(Locale.getDefault())!!
val id: String = model.titleID?.lowercase(Locale.getDefault())!!
if (title.contains(lowerCaseQuery) || id.contains(lowerCaseQuery)) {
filteredModelList.add(model)
}
}
return filteredModelList
}
}
@@ -1,7 +0,0 @@
package com.illusionware.npsbrowser.fragments.mainactivity
import androidx.lifecycle.ViewModel
class MainActivityViewModel : ViewModel() {
// TODO: Implement the ViewModel
}
@@ -0,0 +1,9 @@
package com.illusionware.npsbrowser.model
enum class ConsoleType {
PSVITA,
PS3,
PSP,
PSX,
PSM,
}
@@ -0,0 +1,18 @@
package com.illusionware.npsbrowser.model
data class PackageItem(
val titleId: String,
val region: String,
val name: 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 consoleType: ConsoleType,
val dataType: PackageItemType,
)
@@ -0,0 +1,7 @@
package com.illusionware.npsbrowser.model
enum class PackageItemType {
GAME,
DLC,
THEME,
}
@@ -0,0 +1,51 @@
package com.illusionware.npsbrowser.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.illusionware.npsbrowser.ui.theme.Typography
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NPSAlertDialog(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
title: String,
buttons: @Composable () -> Unit,
content: @Composable () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
) {
Surface (
modifier = Modifier.wrapContentHeight().wrapContentWidth(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(
modifier = modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(text = title, style = Typography.titleLarge)
content()
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
buttons()
}
}
}
}
}
@@ -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,20 @@
package com.illusionware.npsbrowser.ui.components
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.PlainTooltipBox
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NPSIconButton(tooltip: String, onClick: () -> Unit, content: @Composable () -> Unit) {
PlainTooltipBox(tooltip = {
Text(text = tooltip)
}) {
IconButton(onClick = onClick, Modifier.tooltipAnchor()) {
content()
}
}
}
@@ -0,0 +1,23 @@
package com.illusionware.npsbrowser.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun NPSRadioButton(text: String = "Radio Button", onClick: () -> Unit = {}, selected: Boolean = true) {
Box (Modifier.clickable { onClick() }.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = selected, onClick = onClick)
Text(text = text)
}
}
}
@@ -0,0 +1,891 @@
package com.illusionware.npsbrowser.ui.screens
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.ui.theme.ColorAccent
import com.illusionware.npsbrowser.ui.theme.Typography
import com.illusionware.npsbrowser.ui.theme.md_theme_dark_onSurface
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.illusionware.npsbrowser.data.SettingsPreferences
import com.illusionware.npsbrowser.model.ConsoleType
import com.illusionware.npsbrowser.model.PackageItemType
import com.illusionware.npsbrowser.ui.components.NPSIconButton
import com.illusionware.npsbrowser.viewmodels.OnboardingViewModel
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
const val PAGE_COUNT = 3
val readexProFamily = FontFamily(
Font(R.font.readex_pro_extra_light, FontWeight.ExtraLight),
Font(R.font.readex_pro_light, FontWeight.Light),
Font(R.font.readex_pro_regular, FontWeight.Normal),
Font(R.font.readex_pro_medium, FontWeight.Medium),
Font(R.font.readex_pro_semi_bold, FontWeight.SemiBold),
Font(R.font.readex_pro_bold, FontWeight.Bold),
)
@OptIn(ExperimentalFoundationApi::class)
@SuppressLint("SourceLockedOrientationActivity")
@Composable
fun OnBoarding(
navigateToHome: () -> Unit = {},
darkTheme: Boolean = false,
onboardingViewModel: OnboardingViewModel = viewModel(
factory = OnboardingViewModel.Factory
),
settingsViewModel: SettingsViewModel = viewModel(
factory = SettingsViewModel.Factory
),
) {
val systemUiController = rememberSystemUiController()
val pagerState = rememberPagerState(
initialPage = 0,
initialPageOffsetFraction = 0f
)
val scope = rememberCoroutineScope()
val context = LocalContext.current
val settingsPrefs = settingsViewModel.uiState.collectAsStateWithLifecycle().value
val darkIcons = when (pagerState.currentPage) {
0 -> false
else -> true
}
DisposableEffect(systemUiController) {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = darkIcons && !darkTheme
)
val activity = context as Activity
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
onDispose {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = !darkTheme
)
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
HorizontalPager(pageCount = PAGE_COUNT, state = pagerState) { page ->
when (page) {
0 -> OnBoardingInitial(
incrementPage = {
scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
},
)
1 -> OnBoardingSetupTSVs(
page = page,
incrementPage = {
scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
},
skipToEnd = {
scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) }
},
settingsViewModel,
)
// 2 -> OnBoardingStoragePermission(
// page = page,
// incrementPage = {
// scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
// },
// decrementPage = {
// scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) }
// },
// skipToEnd = {
// scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) }
// },
// )
2 -> OnBoardingSetupSettings(
page = page,
incrementPage = {
scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
},
decrementPage = {
scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) }
},
skipToEnd = {
scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) }
},
navigateToHome = navigateToHome,
settingsViewModel,
onboardingViewModel,
settingsPrefs,
)
}
}
}
@Composable
@Preview
fun OnBoardingInitial(incrementPage: () -> Unit = {}) {
val density = LocalDensity.current
var visible by remember { mutableStateOf(false) }
LaunchedEffect(key1 = visible) {
visible = true
}
Scaffold(containerColor = ColorAccent, contentColor = MaterialTheme.colorScheme.onBackground) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.statusBarsPadding(),
verticalArrangement = Arrangement.Center,
) {
Column(
verticalArrangement = Arrangement.spacedBy(56.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
h -> h
} + fadeIn(
initialAlpha = 0.2f
),
) {
Image(
painter = painterResource(id = R.drawable.nps_fg_onboarding),
contentDescription = "App Logo",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 64.dp)
)
}
AnimatedVisibility(
visible = visible,
enter = slideInVertically(tween(delayMillis = 100)) {
with(density) { 50.dp.roundToPx() }
} + fadeIn(
animationSpec = tween(delayMillis = 50),
initialAlpha = 0.0f,
),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(48.dp)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
ProvideTextStyle(value = Typography.displayMedium.copy(color = Color.White)) {
Text(text = "No Ads.")
Text(text = "No Waiting.")
Text(text = "No Bullshit.")
}
}
Text(
text = "Browse and download from a huge library of games, themes, and DLC to your heart's content.",
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp),
style = Typography.titleLarge.copy(color = md_theme_dark_onSurface),
)
}
}
Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(
animationSpec = tween(delayMillis = 200, durationMillis = 500),
initialAlpha = 0.0f,
),
) {
TextButton(
onClick = incrementPage,
colors = ButtonDefaults.textButtonColors(contentColor = Color.White, containerColor = Color(0xFF1A91EC)),
contentPadding = PaddingValues(horizontal = 24.dp)
) {
Text(text = "Get Started")
}
}
}
}
}
}
@Composable
fun OnBoardingSetupTSVs(
page: Int = 1,
incrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {},
settingsViewModel: SettingsViewModel,
) {
Scaffold { padding ->
Column(
Modifier
.padding(padding)
.padding(vertical = 32.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(text = "Set up TSVs", style = Typography.displayMedium)
Spacer(modifier = Modifier.height(8.dp))
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "You can set up your TSV files on this screen",
style = Typography.titleMedium.copy(color = MaterialTheme.colorScheme.outline),
textAlign = TextAlign.Center,
)
Text(
text = "You are expected to provide the TSV files yourself. NPS Browser will NOT provide you with these files",
style = Typography.titleSmall.copy(color = Color(0xFFD63A3A)),
textAlign = TextAlign.Center
)
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.weight(1.0f, fill = false)
.padding(vertical = 16.dp),
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TSVGroup(title = "PS Vita") {
TSVButton(
"Games",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.GAME, it) }
)
TSVButton(
"Themes",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.THEME, it) }
)
TSVButton(
"DLC",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.DLC, it) }
)
}
TSVGroup(title = "PSP") {
TSVButton(
"Games",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSP, PackageItemType.GAME, it) }
)
TSVButton(
"DLC",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSP, PackageItemType.DLC, it) }
)
Box(modifier = Modifier.width(110.dp))
}
TSVGroup(title = "PS3") {
TSVButton(
"Games",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PS3, PackageItemType.GAME, it) }
)
TSVButton(
"DLC",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PS3, PackageItemType.DLC, it) }
)
Box(modifier = Modifier.width(110.dp))
}
TSVGroup(title = "PSX") {
TSVButton(
"Games",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSX, PackageItemType.GAME, it) }
)
}
TSVGroup(title = "PSM") {
TSVButton(
"Games",
onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSM, PackageItemType.GAME, it) }
)
}
}
}
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
OnBoardingBottomButtons(
page = page,
incrementPage = incrementPage,
skipToEnd = skipToEnd,
)
}
}
}
}
}
@Composable
fun ColumnGroup(
title: String,
modifier: Modifier = Modifier,
titleModifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
Column(modifier = modifier) {
Text(
modifier = titleModifier,
text = title,
style = TextStyle.Default.copy(
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
),
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
content = content
)
}
}
@Composable
fun TSVGroup(title: String, content: @Composable RowScope.() -> Unit) {
Column {
Text(
text = title,
style = TextStyle.Default.copy(
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
content = content
)
}
}
@Composable
fun TSVButton(title: String, onFinish: (Uri) -> Unit) {
val context = LocalContext.current
var hasSelected by rememberSaveable { mutableStateOf(false) }
val filePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
) { fileUri ->
if (fileUri != null) {
context.contentResolver.takePersistableUriPermission(
fileUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
onFinish(fileUri)
hasSelected = true
}
}
FilledTonalButton(
onClick = {
filePicker.launch(arrayOf("text/tab-separated-values"))
},
modifier = Modifier
.width(110.dp)
.height(160.dp)
.padding(vertical = 8.dp, horizontal = 0.dp)
.graphicsLayer(clip = false),
shape = MaterialTheme.shapes.medium,
contentPadding = PaddingValues(8.dp),
colors = ButtonDefaults.filledTonalButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground,
containerColor = if (!hasSelected) MaterialTheme.colorScheme.secondaryContainer else Color(0x335EC03C)
),
) {
Layout(
modifier = Modifier.graphicsLayer(clip = false),
content = {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(id = R.drawable.tsv_file),
contentDescription = "Add TSV",
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
tint = MaterialTheme.colorScheme.onBackground,
)
Text(text = title, style = Typography.titleMedium)
}
Icon(
modifier = Modifier.layoutId("selected"),
imageVector = Icons.Filled.CheckCircle,
contentDescription = "Selected",
tint = Color(0xFF5EC03C)
)
}
) { measurables, constraints ->
val buttonPlaceable = measurables[0].measure(constraints)
val iconPlaceable = measurables.first { it.layoutId == "selected" }.measure(constraints)
layout(buttonPlaceable.width, buttonPlaceable.height) {
if (hasSelected) {
iconPlaceable.placeRelative(
x = buttonPlaceable.width - iconPlaceable.width,
y = -8,
)
}
buttonPlaceable.placeRelative(0, 0)
}
}
}
}
//@Composable
//fun OnBoardingStoragePermission(
// page: Int = 2,
// incrementPage: () -> Unit = {},
// decrementPage: () -> Unit = {},
// skipToEnd: () -> Unit = {},
//) {
// val hasPermission = when (PackageManager.PERMISSION_GRANTED) {
// ContextCompat.checkSelfPermission(
// LocalContext.current,
// Manifest.permission.WRITE_EXTERNAL_STORAGE
// ) -> true
// else -> false
// }
//
// val permissionLegacy = rememberLauncherForActivityResult(
// contract = ActivityResultContracts.RequestPermission(),
// ) {}
//
// Scaffold { padding ->
// Column(
// Modifier
// .padding(padding)
// .padding(16.dp)
// .fillMaxSize(),
// verticalArrangement = Arrangement.SpaceBetween
// ) {
// Column(
// Modifier
// .padding(vertical = 32.dp)
// .weight(1.0f)
// .fillMaxHeight(),
// horizontalAlignment = Alignment.CenterHorizontally,
// ) {
// Image(
// painter = painterResource(id = R.drawable.folder_bubbles),
// contentDescription = "Folder Image",
// contentScale = ContentScale.Crop,
// modifier = Modifier.fillMaxWidth()
// )
// Text(
// text = "Storage".uppercase(),
// style = TextStyle.Default.copy(
// fontWeight = FontWeight.Bold,
// fontSize = 28.sp,
// fontFamily = readexProFamily
// ),
// )
// Spacer(modifier = Modifier.height(24.dp))
// Text(
// "Granting storage permission allows us to bypass newer Android limitations and choose any directory as your download directory",
// style = TextStyle.Default.copy(
// fontSize = 20.sp,
// fontFamily = readexProFamily
// ),
// textAlign = TextAlign.Center,
// )
// Spacer(modifier = Modifier
// .fillMaxHeight()
// .weight(1.0f))
// Button(
// onClick = {
// permissionLegacy.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
// },
// modifier = Modifier.fillMaxWidth(),
// enabled = !hasPermission,
// ) {
// Text(
// text = if (hasPermission) "Granted" else "Grant",
// style = TextStyle.Default.copy(
// fontSize = 18.sp,
// fontFamily = readexProFamily
// ),
// )
// }
// }
// Column(Modifier.fillMaxWidth()) {
// OnBoardingBottomButtons(
// page = page,
// incrementPage = incrementPage,
// decrementPage = decrementPage,
// skipToEnd = skipToEnd
// )
// }
// }
// }
//}
@Composable
fun OnBoardingSetupSettings(
page: Int = 2,
incrementPage: () -> Unit = {},
decrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {},
navigateToHome: () -> Unit = {},
settingsViewModel: SettingsViewModel,
onboardingViewModel: OnboardingViewModel,
settingsPrefs: SettingsPreferences,
) {
val clipboard = LocalClipboardManager.current
var hmacKey by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val downloadDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
settingsViewModel.setDownloadDir(dirUri.toString())
}
}
val unpackDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
settingsViewModel.setUnpackDir(dirUri.toString())
}
}
fun setHmacKey(key: String) {
if (key.isNotEmpty()) {
settingsViewModel.setHMACKey(key)
}
}
Scaffold { padding ->
Column(
Modifier
.padding(padding)
.padding(vertical = 32.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Configure App", style = Typography.displayMedium)
Text(
text = "Set the app's settings to your liking and control how it behaves",
style = Typography.titleMedium.copy(color = MaterialTheme.colorScheme.outline),
textAlign = TextAlign.Center,
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.weight(1.0f, fill = false)
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
ColumnGroup(title = "HMAC Key", modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
text = "The HMAC Key is used to decrypt the PS Vita game updates' urls. We cannot provide this key since it's the property of Sony",
style = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
)
OutlinedTextField(
value = hmacKey,
onValueChange = {
hmacKey = it
setHmacKey(it.text)
},
label = { Text(text = "HMAC Key") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailingIcon = {
NPSIconButton(
tooltip = "Paste",
onClick = {
val text = clipboard.getText()?.text ?: ""
hmacKey = TextFieldValue(text, selection = TextRange(text.length))
setHmacKey(text)
}
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = "Paste",
)
}
}
)
}
ColumnGroup(title = "Downloads", titleModifier = Modifier.padding(horizontal = 16.dp)) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = "Choose your desired directory to download and unpack packages",
style = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
)
Column {
Toggleable(
title = "Unpack in download directory",
checked = settingsPrefs.unpackInDownload,
onCheckedChange = {
settingsViewModel.setUnpackInDownload(it)
},
)
Toggleable(
title = "Delete package after unpacking",
checked = settingsPrefs.deleteAfterUnpack,
onCheckedChange = {
settingsViewModel.setDeleteAfterUnpack(it)
},
)
Box(
Modifier
.fillMaxWidth()
.clickable { downloadDirPicker.launch(Uri.EMPTY) }
) {
Column(Modifier.padding(16.dp)) {
Text(
text = "Download Directory",
)
Text(
text = Uri.decode(settingsPrefs.downloadDir).ifEmpty { "None" },
color = MaterialTheme.colorScheme.outline,
)
}
}
Box(
Modifier
.fillMaxWidth()
.clickable { unpackDirPicker.launch(Uri.EMPTY) }
) {
Column(Modifier.padding(16.dp)) {
Text(
text = "Unpack Directory",
)
Text(
text = Uri.decode(settingsPrefs.unpackDir).ifEmpty { "None" },
color = MaterialTheme.colorScheme.outline,
)
}
}
}
}
ColumnGroup(title = "PKG2ZIP", titleModifier = Modifier.padding(horizontal = 16.dp)) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = "Modify the parameters and arguments of pkg2zip",
style = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
)
Toggleable(
title = "Automatically decrypt downloaded content",
checked = settingsPrefs.pkg2zipAutoDecrypt,
onCheckedChange = {
settingsViewModel.setPkg2zipAutoDecrypt(it)
},
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
value = settingsPrefs.pkg2zipParams,
onValueChange = {
settingsViewModel.setPkg2zipParams(it)
}, // TODO: validate args are legal ??
label = { Text(text = "Pkg2zip args") },
placeholder = { Text(text = "-x {pkgFile} \"{zRifKey}\"") },
)
}
}
Column(
modifier = Modifier.padding(horizontal = 16.dp),
) {
OnBoardingBottomButtons(
page = page,
incrementPage = incrementPage,
decrementPage = decrementPage,
skipToEnd = skipToEnd,
navigateToHome = navigateToHome,
settingsViewModel = settingsViewModel,
onboardingViewModel = onboardingViewModel,
)
}
}
}
}
}
@Composable
fun Toggleable(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
Box(
modifier = Modifier
.clickable onCheckedChange@{
onCheckedChange(!checked)
}
.padding(vertical = 16.dp),
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f))
Switch(checked = checked, onCheckedChange = null)
}
}
}
@Composable
fun OnBoardingBottomButtons(
page: Int,
incrementPage: () -> Unit = {},
decrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {},
navigateToHome: () -> Unit = {},
settingsViewModel: SettingsViewModel? = null,
onboardingViewModel: OnboardingViewModel? = null
) {
val finalPage = PAGE_COUNT - 1
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f),
horizontalArrangement = if (page != finalPage) Arrangement.SpaceBetween else Arrangement.End,
) {
if (page != finalPage) {
FilledTonalButton(
onClick = skipToEnd,
colors = ButtonDefaults.filledTonalButtonColors(
contentColor = MaterialTheme.colorScheme.primary
),
contentPadding = PaddingValues(horizontal = 18.dp)
) {
Text(text = "Skip")
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (page != 1) {
Button(onClick = decrementPage, contentPadding = PaddingValues(horizontal = 18.dp)) {
Text(text = "Prev")
}
}
if (page == finalPage) {
Button(
onClick = {
navigateToHome()
onboardingViewModel?.finishOnboarding(settingsViewModel!!.shouldCreateDefaultDir())
},
contentPadding = PaddingValues(horizontal = 18.dp)
) {
Text(text = "Finish")
}
} else {
Button(
onClick = incrementPage,
contentPadding = PaddingValues(horizontal = 18.dp)
) {
Text(text = "Next")
}
}
}
}
}
@@ -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)
}
}
@@ -0,0 +1,581 @@
package com.illusionware.npsbrowser.ui.screens
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.clickable
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.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.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Unarchive
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
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 com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.ui.components.NPSAlertDialog
import com.illusionware.npsbrowser.ui.components.NPSIconButton
import com.illusionware.npsbrowser.ui.components.NPSRadioButton
import com.illusionware.npsbrowser.ui.theme.ColorSecondaryLight
import com.illusionware.npsbrowser.ui.theme.Typography
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.illusionware.npsbrowser.data.SettingsPreferences
import com.illusionware.npsbrowser.data.Theme
import com.illusionware.npsbrowser.model.ConsoleType
import com.illusionware.npsbrowser.model.PackageItemType
private val Themes = hashMapOf(
Theme.LIGHT.ordinal to "Light",
Theme.DARK.ordinal to "Dark",
Theme.SYSTEM.ordinal to "Use System Theme"
)
@Composable
@Preview
fun SettingsScreen(
navigationGoBack: () -> Unit = {},
viewModel: SettingsViewModel = viewModel(
factory = SettingsViewModel.Factory
)
) {
val prefs = viewModel.uiState.collectAsStateWithLifecycle().value
Scaffold (
topBar = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.statusBarsPadding()
.padding(vertical = 8.dp, horizontal = 4.dp),
) {
NPSIconButton(tooltip = "Go Back", onClick = { navigationGoBack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Go Back" )
}
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)
.navigationBarsPadding()
) {
Appearance(viewModel, prefs)
TsvFiles(viewModel, prefs)
Updates(viewModel, prefs)
Downloads(viewModel, prefs)
Pkg2Zip(viewModel, prefs)
}
}
}
}
@Composable
fun Appearance(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
var themeOpenDialog by remember { mutableStateOf(false) }
SettingGroup(title = stringResource(id = R.string.appearance)) {
DialogSetting(
title = stringResource(id = R.string.theme),
value = Themes[prefs.appTheme] ?: "None",
icon = Icons.Outlined.Palette,
onClick = { themeOpenDialog = true }
)
}
if (themeOpenDialog) {
ThemeDialog(
prefs = prefs,
onDismiss = { themeOpenDialog = false },
onThemeChange = {
viewModel.setTheme(it)
}
)
}
}
@Composable
fun TsvFiles(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
var expanded by remember { mutableStateOf(false) }
SettingGroup(title = "TSV Files") {
TSVPicker(
title = "PSV Games",
value = Uri.decode(prefs.psvGames.ifEmpty { "Browse" }),
icon = painterResource(id = R.drawable.vita_icon),
onFilePick = {
viewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.GAME, it)
}
)
TSVPicker(
title = "PSV DLC",
value = Uri.decode(prefs.psvDlc.ifEmpty { "Browse" }),
icon = null,
onFilePick = {
viewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.DLC, it)
}
)
TSVPicker(
title = "PSV Themes",
value = Uri.decode(prefs.psvThemes.ifEmpty { "Browse" }),
icon = null,
onFilePick = {
viewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.THEME, it)
},
)
TSVPicker(
title = "PS3 Games",
value = Uri.decode(prefs.ps3Games).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.ps3_icon),
onFilePick = {
viewModel.setTSVFile(ConsoleType.PS3, PackageItemType.GAME, it)
}
)
if (!expanded) {
DialogSetting(
title = "Expand",
value = "PS3 DLC, PSP Games, PSP DLC, PSX Games, PSM Games",
icon = Icons.Outlined.ArrowDropDown,
singleLine = true,
onClick = { expanded = true },
)
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically(),
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
TSVPicker(
title = "PS3 DLC",
value = Uri.decode(prefs.ps3Dlc).ifEmpty { "Browse" },
icon = null,
onFilePick = {
viewModel.setTSVFile(ConsoleType.PS3, PackageItemType.DLC, it)
}
)
TSVPicker(
title = "PSP Games",
value = Uri.decode(prefs.pspGames).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.psp_icon),
onFilePick = {
viewModel.setTSVFile(ConsoleType.PSP, PackageItemType.GAME, it)
}
)
TSVPicker(
title = "PSP DLC",
value = Uri.decode(prefs.pspDlc).ifEmpty { "Browse" },
icon = null,
onFilePick = {
viewModel.setTSVFile(ConsoleType.PSP, PackageItemType.DLC, it)
}
)
TSVPicker(
title = "PSX Games",
value = Uri.decode(prefs.psxGames).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.psx_icon),
onFilePick = {
viewModel.setTSVFile(ConsoleType.PSX, PackageItemType.GAME, it)
}
)
TSVPicker(
title = "PSM Games",
value = Uri.decode(prefs.psmGames).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.psm_icon),
onFilePick = {
viewModel.setTSVFile(ConsoleType.PSM, PackageItemType.GAME, it)
}
)
}
}
}
}
@Composable
fun TSVPicker(
title: String,
value: String,
icon: Painter? = null,
onFilePick: (Uri) -> Unit,
) {
val context = LocalContext.current
val filePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
) { fileUri ->
if (fileUri != null) {
context.contentResolver.takePersistableUriPermission(
fileUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
onFilePick(fileUri)
}
}
DialogSetting(
title = title,
value = value,
icon = icon,
onClick = {
filePicker.launch(arrayOf("text/tab-separated-values"))
}
)}
@Composable
fun Updates(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
val clipboard = LocalClipboardManager.current
var hmacOpenDialog by remember { mutableStateOf(false) }
var dialogHmacKey by remember { mutableStateOf("") }
SideEffect {
dialogHmacKey = prefs.hmacKey
}
SettingGroup(title = "Updates") {
DialogSetting(
title = "HMAC Key",
value = prefs.hmacKey.ifEmpty { "None" },
icon = Icons.Outlined.Key,
onClick = { hmacOpenDialog = true }
)
}
if (hmacOpenDialog) {
NPSAlertDialog(
onDismiss = { hmacOpenDialog = false },
title = "HMAC Key",
buttons = {
TextButton(onClick = { hmacOpenDialog = false }) {
Text(text = "Cancel")
}
TextButton(onClick = {
viewModel.setHMACKey(dialogHmacKey)
hmacOpenDialog = false
}) {
Text(text = "OK")
}
}
) {
OutlinedTextField(
value = dialogHmacKey,
onValueChange = { dialogHmacKey = it },
label = { Text(text = "HMAC Key") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailingIcon = {
NPSIconButton(
tooltip = "Paste",
onClick = { dialogHmacKey = clipboard.getText()?.text ?: "" }
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = "Paste",
)
}
}
)
}
}
}
@Composable
fun Downloads(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
val downloadDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
viewModel.setDownloadDir(dirUri.toString())
}
}
val unpackDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
viewModel.setUnpackDir(dirUri.toString())
}
}
SettingGroup(title = "Downloads") {
ToggleSetting(
title = "Unpack in Download Directory",
checked = prefs.unpackInDownload,
onClick = {
viewModel.setUnpackInDownload(it)
}
)
ToggleSetting(
title = "Delete package after unpacking",
checked = prefs.deleteAfterUnpack,
onClick = {
viewModel.setDeleteAfterUnpack(it)
}
)
DialogSetting(
title = "Download Directory",
value = Uri.decode(prefs.downloadDir).ifEmpty { "None" },
icon = Icons.Outlined.FileDownload,
onClick = {
downloadDirPicker.launch(Uri.EMPTY)
}
)
DialogSetting(
title = "Unpack Directory",
value = Uri.decode(prefs.unpackDir).ifEmpty { "None" },
icon = Icons.Outlined.Unarchive,
enabled = !prefs.unpackInDownload,
onClick = {
unpackDirPicker.launch(Uri.EMPTY)
}
)
}
}
@Composable
fun Pkg2Zip(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
var pkg2zipOpenDialog by remember { mutableStateOf(false) }
var dialogPkg2zipParams by remember { mutableStateOf("") }
SideEffect {
dialogPkg2zipParams = prefs.pkg2zipParams
}
SettingGroup(title = "Pkg2Zip") {
ToggleSetting(
title = "Automatically decrypt downloaded content",
checked = prefs.pkg2zipAutoDecrypt,
onClick = {
viewModel.setPkg2zipAutoDecrypt(it)
}
)
DialogSetting(
title = "Decryption params",
value = prefs.pkg2zipParams.ifEmpty { "None" },
icon = painterResource(
id = R.drawable.lock_open
),
onClick = { pkg2zipOpenDialog = true }
)
}
if (pkg2zipOpenDialog) {
NPSAlertDialog(
onDismiss = { pkg2zipOpenDialog = false },
title = "PKG2ZIP Args",
buttons = {
TextButton(onClick = { pkg2zipOpenDialog = false }) {
Text(text = "Cancel")
}
TextButton(onClick = {
viewModel.setPkg2zipParams(dialogPkg2zipParams)
pkg2zipOpenDialog = false
}) {
Text(text = "OK")
}
}
) {
OutlinedTextField(
value = dialogPkg2zipParams,
onValueChange = { dialogPkg2zipParams = it },
label = { Text(text = "PKG2ZIP Args") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
}
}
}
@Composable
fun ThemeDialog(
prefs: SettingsPreferences,
onDismiss: () -> Unit,
onThemeChange: (v: Theme) -> Unit
) {
NPSAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.theme),
buttons = {
TextButton(onClick = onDismiss) {
Text(text = "Cancel")
}
},
modifier = Modifier.fillMaxWidth()
) {
Column {
NPSRadioButton(
text = "Light",
onClick = {
onThemeChange(Theme.LIGHT)
},
selected = prefs.appTheme == Theme.LIGHT.ordinal,
)
NPSRadioButton(
text = "Dark",
onClick = {
onThemeChange(Theme.DARK)
},
selected = prefs.appTheme == Theme.DARK.ordinal,
)
NPSRadioButton(
text = "Use System Theme",
onClick = {
onThemeChange(Theme.SYSTEM)
},
selected = prefs.appTheme == Theme.SYSTEM.ordinal,
)
}
}
}
@Composable
fun SettingGroup(title: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header(title)
Spacer(modifier = Modifier.size(2.dp))
content()
}
}
@Composable
fun Header(title: String) {
Row(Modifier.padding(start = 56.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.primary)
}
}
@Composable
fun DialogSetting(title: String, value: String, icon: Painter?, onClick: () -> Unit) {
Box(
Modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
if (icon != null) {
Icon(painter = icon, contentDescription = null, modifier = Modifier.width(56.dp))
} else {
Box(modifier = Modifier.width(56.dp))
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(text = value, color = ColorSecondaryLight, fontSize = 14.sp)
}
}
}
}
@Composable
fun DialogSetting(
title: String,
value: String,
icon: ImageVector?,
onClick: () -> Unit,
singleLine : Boolean = false,
enabled: Boolean = true
) {
Box(
Modifier
.fillMaxWidth()
.alpha(if (enabled) 1f else 0.5f)
.then(if (enabled) Modifier.clickable { onClick() } else Modifier)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
if (icon != null) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.width(56.dp))
} else {
Box(modifier = Modifier.width(56.dp))
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(
text = value,
color = ColorSecondaryLight,
fontSize = 14.sp,
maxLines = if (singleLine) 1 else Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 24.dp)
)
}
}
}
}
@Composable
fun ToggleSetting(title: String, icon: ImageVector? = null, checked: Boolean, onClick: (Boolean) -> Unit) {
Box(
modifier = Modifier.clickable onClick@{
onClick(!checked)
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp),
) {
if (icon != null) {
Icon(imageVector = icon, contentDescription = null, Modifier.width(56.dp))
} else {
Box(modifier = Modifier.width(56.dp))
}
Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f))
Switch(checked = checked, onCheckedChange = null)
}
}
}
@@ -0,0 +1,90 @@
package com.illusionware.npsbrowser.ui.theme
import androidx.compose.ui.graphics.Color
val ColorPrimaryLight = Color(0xFFFFFFFF)
val ColorPrimaryVariantLight = Color(0xFFEFEFEF)
val ColorOnPrimaryLight = Color(0xFF000000)
val ColorSecondaryLight = Color(0xFF999999)
val ColorSecondaryVariantLight = Color(0xFF888888)
val ColorOnSecondaryLight = Color(0xFF111111)
val ColorAmoledLight = Color(0xFFFFFFFF)
val ColorPrimaryDark = Color(0xFF222222)
val ColorPrimaryVariantDark = Color(0xFF202020)
val ColorOnPrimaryDark = Color(0xFFFFFFFF)
val ColorSecondaryDark = Color(0xFF696969)
val ColorSecondaryVariantDark = Color(0xFF515151)
val ColorOnSecondaryDark = Color(0xDFFFFFFF)
val ColorAmoledDark = Color(0xFF000000)
// Works on both Dark and Light
val ColorAccent = Color(0xFF545FBF)
val ColorError = Color(0xFFF93422)
// Light Mode
val md_theme_light_primary = Color(0xFF5951B3)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFE4DFFF)
val md_theme_light_onPrimaryContainer = Color(0xFF140067)
val md_theme_light_secondary = Color(0xFF5E5C71)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFE4DFF9)
val md_theme_light_onSecondaryContainer = Color(0xFF1B192C)
val md_theme_light_tertiary = Color(0xFF5754A8)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFE3DFFF)
val md_theme_light_onTertiaryContainer = Color(0xFF110563)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF1C1B1F)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF1C1B1F)
val md_theme_light_surfaceVariant = Color(0xFFE5E1EC)
val md_theme_light_onSurfaceVariant = Color(0xFF47464F)
val md_theme_light_outline = Color(0xFF787680)
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
val md_theme_light_inverseSurface = Color(0xFF313034)
val md_theme_light_inversePrimary = Color(0xFFC5C0FF)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF5951B3)
val md_theme_light_outlineVariant = Color(0xFFC8C5D0)
val md_theme_light_scrim = Color(0xFF000000)
// Dark Mode
val md_theme_dark_primary = Color(0xFFC5C0FF)
val md_theme_dark_onPrimary = Color(0xFF2A1D82)
val md_theme_dark_primaryContainer = Color(0xFF413899)
val md_theme_dark_onPrimaryContainer = Color(0xFFE4DFFF)
val md_theme_dark_secondary = Color(0xFFC8C4DC)
val md_theme_dark_onSecondary = Color(0xFF302E41)
val md_theme_dark_secondaryContainer = Color(0xFF464559)
val md_theme_dark_onSecondaryContainer = Color(0xFFE4DFF9)
val md_theme_dark_tertiary = Color(0xFFC3C0FF)
val md_theme_dark_onTertiary = Color(0xFF282377)
val md_theme_dark_tertiaryContainer = Color(0xFF3F3C8F)
val md_theme_dark_onTertiaryContainer = Color(0xFFE3DFFF)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF1C1B1F)
val md_theme_dark_onBackground = Color(0xFFE5E1E6)
val md_theme_dark_surface = Color(0xFF1C1B1F)
val md_theme_dark_onSurface = Color(0xFFE5E1E6)
val md_theme_dark_surfaceVariant = Color(0xFF47464F)
val md_theme_dark_onSurfaceVariant = Color(0xFFC8C5D0)
val md_theme_dark_outline = Color(0xFF928F99)
val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F)
val md_theme_dark_inverseSurface = Color(0xFFE5E1E6)
val md_theme_dark_inversePrimary = Color(0xFF5951B3)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFC5C0FF)
val md_theme_dark_outlineVariant = Color(0xFF47464F)
val md_theme_dark_scrim = Color(0xFF000000)
val seed = Color(0xFF5E56B8)
@@ -0,0 +1,122 @@
package com.illusionware.npsbrowser.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import com.google.accompanist.systemuicontroller.rememberSystemUiController
private val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val DarkColorScheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
)
@Composable
fun NPSBrowserTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val systemUiController = rememberSystemUiController()
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
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
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = !darkTheme
)
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
@@ -0,0 +1,34 @@
package com.illusionware.npsbrowser.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -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,52 @@
package com.illusionware.npsbrowser.viewmodels
import android.os.Environment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.illusionware.npsbrowser.NPSApp
import com.illusionware.npsbrowser.data.OnboardingPreferences
import com.illusionware.npsbrowser.data.OnboardingPreferencesRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.io.File
class OnboardingViewModel(
private val onboardingPreferencesRepository: OnboardingPreferencesRepository
): ViewModel() {
val uiState: StateFlow<OnboardingPreferences> =
onboardingPreferencesRepository.getPreferences().stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(2_000),
OnboardingPreferences(
false,
)
)
fun finishOnboarding(shouldCreateDefaultDir: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
if (shouldCreateDefaultDir) {
val dir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "NPSBrowser")
if (!dir.exists()) {
dir.mkdir()
}
}
onboardingPreferencesRepository.setSeenOnboarding(true)
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as NPSApp)
OnboardingViewModel(application.onboardingPreferencesRepository)
}
}
}
}
@@ -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)
}
}
@@ -0,0 +1,193 @@
package com.illusionware.npsbrowser.viewmodels
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.github.doyaaaaaken.kotlincsv.dsl.context.ExcessFieldsRowBehaviour
import com.github.doyaaaaaken.kotlincsv.dsl.context.InsufficientFieldsRowBehaviour
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
import com.illusionware.npsbrowser.NPSApp
import com.illusionware.npsbrowser.data.SettingsPreferencesRepository
import com.illusionware.npsbrowser.model.ConsoleType
import com.illusionware.npsbrowser.model.PackageItem
import com.illusionware.npsbrowser.model.PackageItemType
import kotlinx.coroutines.Dispatchers
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(
val packages: ArrayList<PackageItem> = ArrayList(),
val error: String = "",
val isLoading: Boolean = true,
)
class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesRepository): ViewModel() {
private val _uiState = MutableStateFlow(PackageListUiState())
val uiState: StateFlow<PackageListUiState> = _uiState.asStateFlow()
private val tsvReader = csvReader {
charset = "UTF-8"
quoteChar = '"'
delimiter = '\t'
excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.TRIM
insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.EMPTY_STRING
}
init {
viewModelScope.launch(Dispatchers.IO) {
val list = ArrayList<PackageItem>()
settingsRepo.availableTsvs().forEach {
list += parsePackageFromTSV(it, app)
}
list.sortBy { it.name }
_uiState.emit(PackageListUiState(isLoading = false, packages = list))
settingsRepo.tsvFiles().collect { tsv ->
_uiState.emit(PackageListUiState())
val alreadyExists = list.find { item ->
item.consoleType == tsv.second && item.dataType == tsv.third
}
if (alreadyExists != null) {
list.removeAll { item ->
item.consoleType == tsv.second && item.dataType == tsv.third
}
}
list.addAll(parsePackageFromTSV(tsv, app))
list.sortBy { it.name }
_uiState.emit(PackageListUiState(isLoading = false, packages = list))
}
}
}
private fun parsePackageFromTSV(
it: Triple<String, ConsoleType, PackageItemType>, app: NPSApp
): ArrayList<PackageItem> {
val list = ArrayList<PackageItem>()
val fileUri = it.first
val console = it.second
val type = it.third
val file: InputStream?
try {
file = app.contentResolver.openInputStream(Uri.parse(fileUri))
} catch (e: Exception) {
throw e
}
val tsv = tsvReader.readAllWithHeader(file!!)
tsv.forEach { row ->
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"]?.trim() ?: "",
pkgUrl,
contentId,
modificationDate,
size,
sha256,
zRif,
rap,
minFW,
console,
type,
)
)
}
file.close()
return list
}
private fun bytesIntoHumanReadable(bytes: Long): String {
val kilobyte: Long = 1024
val megabyte = kilobyte * 1024
val gigabyte = megabyte * 1024
val terabyte = gigabyte * 1024
return if (bytes in 0 until kilobyte) {
"$bytes" + "B"
} else if (bytes in kilobyte until megabyte) {
String.format("%.2f", (bytes.toDouble() / kilobyte.toDouble())) + "KB"
} else if (bytes in megabyte until gigabyte) {
String.format("%.2f", (bytes.toDouble() / megabyte.toDouble())) + "MB"
} else if (bytes in gigabyte until terabyte) {
String.format("%.2f", (bytes.toDouble() / gigabyte.toDouble())) + "GB"
} else if (bytes >= terabyte) {
String.format("%.2f", (bytes.toDouble() / terabyte.toDouble())) + "TB"
} else {
"$bytes" + "B"
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as NPSApp)
PackageListViewModel(application, application.settingsPreferencesRepository)
}
}
}
}
@@ -0,0 +1,138 @@
package com.illusionware.npsbrowser.viewmodels
import android.net.Uri
import android.os.Environment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.illusionware.npsbrowser.NPSApp
import com.illusionware.npsbrowser.data.ItemLayout
import com.illusionware.npsbrowser.data.SettingsPreferences
import com.illusionware.npsbrowser.data.SettingsPreferencesRepository
import com.illusionware.npsbrowser.data.Theme
import com.illusionware.npsbrowser.model.ConsoleType
import com.illusionware.npsbrowser.model.PackageItemType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
val uiState: StateFlow<SettingsPreferences> =
settingsPrefsRepo.getPreferences().stateIn(viewModelScope, SharingStarted.Lazily, SettingsPreferences(
Theme.SYSTEM.ordinal,
ItemLayout.LIST.ordinal,
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
false,
false,
"",
true,
))
fun availableTsvs(): MutableList<Pair<ConsoleType, PackageItemType>> {
val list = mutableListOf<Pair<ConsoleType, PackageItemType>>()
if (uiState.value.psvGames.isNotEmpty()) list.add(Pair(ConsoleType.PSVITA, PackageItemType.GAME))
if (uiState.value.ps3Games.isNotEmpty()) list.add(Pair(ConsoleType.PS3, PackageItemType.GAME))
if (uiState.value.pspGames.isNotEmpty()) list.add(Pair(ConsoleType.PSP, PackageItemType.GAME))
if (uiState.value.psxGames.isNotEmpty()) list.add(Pair(ConsoleType.PSX, PackageItemType.GAME))
if (uiState.value.psmGames.isNotEmpty()) list.add(Pair(ConsoleType.PSM, PackageItemType.GAME))
return list
}
suspend fun getAppTheme(): Int {
return settingsPrefsRepo.getPreferences().first().appTheme
}
fun shouldCreateDefaultDir(): Boolean {
val defaultDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/NPSBrowser"
return uiState.value.downloadDir == defaultDir
|| uiState.value.unpackDir == defaultDir
}
fun setTSVFile(console: ConsoleType, itemType: PackageItemType, file: Uri) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setTSVFile(console, itemType, file)
}
}
fun setTheme(theme: Theme) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setTheme(theme)
}
}
fun setLayout(layout: ItemLayout) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setLayout(layout)
}
}
fun setHMACKey(hmacKey: String) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setHMACKey(hmacKey)
}
}
fun setDownloadDir(downloadDir: String) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setDownloadDir(downloadDir)
}
}
fun setUnpackDir(unpackDir: String) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setUnpackDir(unpackDir)
}
}
fun setUnpackInDownload(unpackInDownload: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setUnpackInDownload(unpackInDownload)
}
}
fun setDeleteAfterUnpack(deleteAfterUnpack: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setDeleteAfterUnpack(deleteAfterUnpack)
}
}
fun setPkg2zipParams(pkg2zipParams: String) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setPkg2zipParams(pkg2zipParams)
}
}
fun setPkg2zipAutoDecrypt(pkg2zipAutoDecrypt: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
settingsPrefsRepo.setPkg2zipAutoDecrypt(pkg2zipAutoDecrypt)
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as NPSApp)
SettingsViewModel(application.settingsPreferencesRepository)
}
}
}
}
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@android:color/holo_blue_dark" />
<stroke android:width="2dip" android:color="@android:color/holo_blue_dark"/>
<corners android:radius="10dp" />
</shape>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@android:color/holo_red_dark" />
<stroke android:width="2dip" android:color="@android:color/holo_red_dark"/>
<corners android:radius="10dp" />
</shape>
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color -->
<item android:drawable="@drawable/splash_gradient"/>
<!-- App logo -->
<item
android:drawable="@drawable/ic_nps_fg_no_shadow"
android:gravity="center"
android:width="400dp"
android:height="400dp" />
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,20 @@
<vector android:height="256dp" android:viewportHeight="100"
android:viewportWidth="100" android:width="256dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ee3e54" android:pathData="M13,27A2,2 0,1 0,13 31A2,2 0,1 0,13 27Z"/>
<path android:fillColor="#f1bc19" android:pathData="M77,12A1,1 0,1 0,77 14A1,1 0,1 0,77 12Z"/>
<path android:fillColor="#fce0a2" android:pathData="M50,13A37,37 0,1 0,50 87A37,37 0,1 0,50 13Z"/>
<path android:fillColor="#f1bc19" android:pathData="M83,11A4,4 0,1 0,83 19A4,4 0,1 0,83 11Z"/>
<path android:fillColor="#ee3e54" android:pathData="M87,22A2,2 0,1 0,87 26A2,2 0,1 0,87 22Z"/>
<path android:fillColor="#fbcd59" android:pathData="M81,74A2,2 0,1 0,81 78,2 2,0 1,0 81,74zM15,59A4,4 0,1 0,15 67,4 4,0 1,0 15,59z"/>
<path android:fillColor="#ee3e54" android:pathData="M25,85A2,2 0,1 0,25 89A2,2 0,1 0,25 85Z"/>
<path android:fillColor="#fff" android:pathData="M18.5,51A2.5,2.5 0,1 0,18.5 56A2.5,2.5 0,1 0,18.5 51Z"/>
<path android:fillColor="#f1bc19" android:pathData="M21,66A1,1 0,1 0,21 68A1,1 0,1 0,21 66Z"/>
<path android:fillColor="#fff" android:pathData="M80,33A1,1 0,1 0,80 35A1,1 0,1 0,80 33Z"/>
<path android:fillColor="#cdcbbd" android:pathData="M29.51,69.3c-0.5,0 -1.13,-0.14 -1.48,-1.27c-0.27,-0.87 -0.27,-1.95 -0.27,-2.65L27.7,37.8v-3.82c0,-1.69 1.32,-3.07 2.94,-3.07h17.65c1.66,0 3.01,1.46 3.01,3.24v1.46c0,1.24 0.97,2.26 2.16,2.26h16.09c0.42,0 0.76,0.38 0.76,0.86v23.57c0,0.33 0.01,0.75 0.03,1.21c0.05,1.7 0.13,4.46 -0.44,5.8H29.51z"/>
<path android:fillColor="#472b29" android:pathData="M48.29,31.6c1.27,0 2.31,1.14 2.31,2.54v1.46c0,1.63 1.28,2.96 2.86,2.96l16.08,-0c0.01,0 0.07,0.05 0.07,0.16v23.57c0,0.34 0.01,0.76 0.03,1.23c0.04,1.29 0.11,3.74 -0.24,5.08h-39.88c-0.27,0 -0.57,0 -0.81,-0.79c-0.24,-0.77 -0.24,-1.78 -0.24,-2.44l0,-0.09L28.4,37.8v-3.82c0,-1.31 1.01,-2.37 2.24,-2.37H48.29M48.29,30.2H30.65c-2.01,0 -3.64,1.7 -3.64,3.77v3.82l0.05,27.48C27.05,66.7 27,70 29.51,70h40.8C71.37,68.36 71,64.1 71,62.28V38.72c0,-0.86 -0.65,-1.56 -1.46,-1.56H53.46c-0.81,0 -1.46,-0.7 -1.46,-1.56v-1.46C52,31.98 50.33,30.2 48.29,30.2L48.29,30.2z"/>
<path android:fillColor="#472b29" android:pathData="M41.5,35h-11c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5h11c0.28,0 0.5,0.22 0.5,0.5S41.78,35 41.5,35z"/>
<path android:fillColor="#472b29" android:pathData="M48.5,35h-4c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5h4c0.28,0 0.5,0.22 0.5,0.5S48.78,35 48.5,35z"/>
<path android:fillColor="#fdfcee" android:pathData="M29.48,69.3c-0.61,0 -1.11,-0.52 -1.14,-1.18L34.03,42.7h41.92l-5.64,25.37c-0.01,0.05 -0.02,0.1 -0.02,0.15c0,0.29 -0.11,0.57 -0.3,0.77c-0.18,0.19 -0.43,0.3 -0.68,0.3H29.48z"/>
<path android:fillColor="#472b29" android:pathData="M75.07,43.4l-5.45,24.52c-0.02,0.1 -0.03,0.2 -0.03,0.31c0,0.12 -0.04,0.23 -0.12,0.3c-0.05,0.05 -0.11,0.08 -0.17,0.08H29.48c-0.21,0 -0.39,-0.19 -0.43,-0.44L34.59,43.4H75.07M76.82,42H33.47l-5.83,26.05c0,1.08 0.82,1.95 1.84,1.95h39.83c0.93,0 1.68,-0.8 1.68,-1.78L76.82,42L76.82,42z"/>
<path android:fillColor="#472b29" android:pathData="M50.5,55h-14c-0.15,0 -0.3,-0.07 -0.39,-0.19s-0.13,-0.28 -0.09,-0.43l1.5,-6C37.57,48.16 37.77,48 38,48h14c0.15,0 0.3,0.07 0.39,0.19s0.13,0.28 0.09,0.43l-1.5,6C50.93,54.84 50.73,55 50.5,55zM37.14,54h12.97l1.25,-5H38.39L37.14,54z"/>
</vector>
File diff suppressed because one or more lines are too long
@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFCA00"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
</vector>
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M208,76L100,76L100,56a28,28 0,0 1,28 -28c13.51,0 25.65,9.62 28.24,22.39a12,12 0,1 0,23.52 -4.78C174.87,21.5 153.1,4 128,4A52.06,52.06 0,0 0,76 56L76,76L48,76A20,20 0,0 0,28 96L28,208a20,20 0,0 0,20 20L208,228a20,20 0,0 0,20 -20L228,96A20,20 0,0 0,208 76ZM204,204L52,204L52,100L204,100ZM112,152a16,16 0,1 1,16 16A16,16 0,0 1,112 152Z"
android:fillColor="#000000"/>
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector android:height="24dp" android:viewportHeight="12"
android:viewportWidth="12" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h12v12h-12z"/>
<path android:fillColor="#000000" android:pathData="M6,1C3.24,1 1,3.24 1,6C1,8.76 3.24,11 6,11C8.76,11 11,8.76 11,6C11,3.24 8.76,1 6,1ZM6,10C3.79,10 2,8.21 2,6C2,5.075 2.315,4.225 2.845,3.55L8.45,9.155C7.775,9.685 6.925,10 6,10ZM9.155,8.45L3.55,2.845C4.225,2.315 5.075,2 6,2C8.21,2 10,3.79 10,6C10,6.925 9.685,7.775 9.155,8.45Z"/>
<group>
<clip-path android:pathData="M2,2h8v8h-8z"/>
<path android:fillColor="#000000" android:fillType="evenOdd" android:pathData="M5.2,4.8C5.2,4.38 5.332,3.972 5.577,3.631C5.823,3.291 6.169,3.036 6.567,2.903C6.965,2.77 7.395,2.766 7.795,2.891C8.196,3.016 8.547,3.264 8.799,3.599C9.051,3.934 9.191,4.341 9.2,4.76C9.208,5.18 9.084,5.591 8.846,5.936C8.608,6.282 8.267,6.543 7.872,6.684C7.477,6.825 7.047,6.838 6.644,6.722L5.883,7.483C5.808,7.558 5.706,7.6 5.6,7.6H5.2V8C5.2,8.106 5.158,8.208 5.083,8.283C5.008,8.358 4.906,8.4 4.8,8.4H4.4V8.8C4.4,8.906 4.358,9.008 4.283,9.083C4.208,9.158 4.106,9.2 4,9.2H3.2C3.094,9.2 2.992,9.158 2.917,9.083C2.842,9.008 2.8,8.906 2.8,8.8V8C2.8,7.894 2.842,7.792 2.917,7.717L5.278,5.356C5.226,5.175 5.2,4.988 5.2,4.8ZM7.2,3.6C7.12,3.6 7.044,3.632 6.988,3.688C6.932,3.744 6.9,3.82 6.9,3.9C6.9,3.98 6.932,4.056 6.988,4.112C7.044,4.168 7.12,4.2 7.2,4.2C7.359,4.2 7.512,4.263 7.624,4.376C7.737,4.488 7.8,4.641 7.8,4.8C7.8,4.88 7.832,4.956 7.888,5.012C7.944,5.068 8.02,5.1 8.1,5.1C8.18,5.1 8.256,5.068 8.312,5.012C8.368,4.956 8.4,4.88 8.4,4.8C8.4,4.482 8.274,4.177 8.049,3.951C7.823,3.726 7.518,3.6 7.2,3.6Z"/>
</group>
</group>
</vector>
+15
View File
@@ -0,0 +1,15 @@
<vector android:height="24dp" android:viewportHeight="12"
android:viewportWidth="12" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h12v12h-12z"/>
<path android:fillColor="#000000" android:pathData="M6,1C3.24,1 1,3.24 1,6C1,8.76 3.24,11 6,11C8.76,11 11,8.76 11,6C11,3.24 8.76,1 6,1ZM6,10C3.79,10 2,8.21 2,6C2,5.075 2.315,4.225 2.845,3.55L8.45,9.155C7.775,9.685 6.925,10 6,10ZM9.155,8.45L3.55,2.845C4.225,2.315 5.075,2 6,2C8.21,2 10,3.79 10,6C10,6.925 9.685,7.775 9.155,8.45Z"/>
<path android:fillColor="#00000000"
android:pathData="M5.822,4.231L6.441,3.612C6.703,3.374 7.046,3.245 7.401,3.253C7.755,3.261 8.093,3.406 8.343,3.657C8.594,3.907 8.739,4.245 8.747,4.599C8.755,4.953 8.626,5.297 8.387,5.559L7.503,6.441C7.376,6.569 7.224,6.67 7.057,6.739C6.891,6.809 6.712,6.844 6.531,6.844C6.351,6.844 6.172,6.809 6.005,6.739C5.838,6.67 5.687,6.569 5.559,6.441"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1"/>
<path android:fillColor="#00000000"
android:pathData="M6.178,7.769L5.559,8.387C5.297,8.626 4.954,8.755 4.599,8.747C4.245,8.739 3.907,8.594 3.657,8.343C3.406,8.093 3.261,7.755 3.253,7.401C3.245,7.047 3.374,6.703 3.613,6.441L4.497,5.559C4.624,5.431 4.776,5.33 4.943,5.261C5.109,5.191 5.288,5.156 5.469,5.156C5.649,5.156 5.828,5.191 5.995,5.261C6.162,5.33 6.313,5.431 6.441,5.559"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1"/>
</group>
</vector>
@@ -0,0 +1,118 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="51.512dp"
android:height="31.41dp"
android:viewportWidth="51.512"
android:viewportHeight="31.41">
<path
android:pathData="m14.423,19.84v-8.27h2.22c1,1.85 2.94,5.2 3.32,6.1v0c-0.09,-0.86 -0.09,-2.29 -0.09,-3.65v-2.45h1.62v8.27h-2.12c-0.89,-1.62 -3,-5.47 -3.42,-6.42v0c0.06,0.72 0.08,2.42 0.08,3.9v2.52z"
android:fillColor="#fff"
android:strokeColor="#00000000"/>
<path
android:pathData="m23.582,11.57h3.42c1.76,0 3,1 3,2.56 0,1.79 -1.36,2.62 -3,2.62h-1.63v3.09h-1.79zM25.372,15.33h1.37c0.82,0 1.41,-0.32 1.41,-1.17 0,-0.85 -0.62,-1.17 -1.38,-1.17h-1.4z"
android:fillColor="#fff"
android:strokeColor="#00000000"/>
<path
android:pathData="m32.592,17.435a1.54,1.54 0,0 0,1.74 1.2c1,0 1.37,-0.38 1.37,-1 0,-0.62 -0.4,-1 -1.82,-1.3 -2.25,-0.53 -2.8,-1.35 -2.8,-2.46 0,-1.44 1.07,-2.42 3,-2.42 2.2,0 3.07,1.18 3.19,2.39h-1.81c-0.09,-0.51 -0.37,-1.06 -1.43,-1.06 -0.73,0 -1.17,0.3 -1.17,0.88 0,0.58 0.35,0.81 1.69,1.12 2.4,0.57 2.93,1.46 2.93,2.63 0,1.51 -1.14,2.54 -3.33,2.54 -2.19,0 -3.2,-1 -3.4,-2.54z"
android:fillColor="#fff"
android:strokeColor="#00000000"/>
<path
android:pathData="m10.951,3.846v0.75z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m3.621,13.07h1.54,0.06l0.71,0.73a0.14,0.14 0,0 1,0 0.19l-0.71,0.73a0.09,0.09 0,0 1,-0.06 0h-1.54a0.1,0.1 0,0 1,-0.09 -0.11v-1.43a0.1,0.1 0,0 1,0.09 -0.11z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m3.603,12.694 l-0.018,0.002c-0.258,0.025 -0.455,0.265 -0.428,0.523l-0.002,-0.039v1.43l0.002,-0.039c-0.027,0.258 0.169,0.497 0.428,0.521l0.018,0.002h1.559l-0.125,-0.021c0.1,0.035 0.21,0.035 0.311,0l0.082,-0.029 0.773,-0.795 0.004,-0.004c0.18,-0.195 0.18,-0.504 0,-0.699l-0.004,-0.004 -0.822,-0.848h-0.219zM3.905,13.444h1.156l0.438,0.451 -0.438,0.449h-1.156z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m5.891,17.05v-1.54a0.05,0.05 0,0 1,0 -0.05l0.73,-0.72a0.17,0.17 0,0 1,0.19 0l0.74,0.72a0.08,0.08 0,0 1,0 0.05v1.54a0.11,0.11 0,0 1,-0.11 0.1h-1.43a0.11,0.11 0,0 1,-0.12 -0.1z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m6.411,14.429 l-0.029,0.02 -0.791,0.781 -0.025,0.043c-0.076,0.131 -0.076,0.295 0,0.426l-0.049,-0.188v1.557,0.016c0.023,0.264 0.266,0.466 0.529,0.441h1.395c0.247,0 0.462,-0.193 0.484,-0.439v-0.018,-1.557l-0.018,0.117c0.03,-0.092 0.03,-0.193 0,-0.285l-0.029,-0.088 -0.83,-0.807 -0.027,-0.02c-0.183,-0.123 -0.426,-0.123 -0.609,0zM6.718,15.171 L7.175,15.618v1.156h-0.908v-1.158z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m9.871,14.78h-1.53a0.06,0.06 0,0 1,-0.06 0L7.561,14.05a0.17,0.17 0,0 1,0 -0.19l0.72,-0.73a0.06,0.06 0,0 1,0.06 0h1.53a0.1,0.1 0,0 1,0.1 0.11L9.971,14.7a0.1,0.1 0,0 1,-0.1 0.08z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m8.341,12.755 l0.188,0.051c-0.134,-0.077 -0.301,-0.077 -0.436,0l-0.043,0.025 -0.781,0.791 -0.02,0.029c-0.123,0.183 -0.123,0.424 0,0.607l0.02,0.029 0.781,0.791 0.043,0.025c0.134,0.077 0.301,0.077 0.436,0l-0.188,0.051h1.521c0.227,0.005 0.431,-0.159 0.477,-0.381l0.008,-0.037v-1.498l-0.002,0.039c0.028,-0.275 -0.201,-0.525 -0.477,-0.523zM8.439,13.505h1.158v0.9h-1.158l-0.445,-0.451z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m7.601,10.8v1.54a0.14,0.14 0,0 1,0 0.06l-0.74,0.71a0.14,0.14 0,0 1,-0.19 0L5.941,12.4a0.09,0.09 0,0 1,0 -0.06L5.941,10.8c0,-0.05 0,-0.09 0.12,-0.09h1.48a0.1,0.1 0,0 1,0.06 0.09z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m6.062,10.335c-0.102,0 -0.249,0.004 -0.381,0.146 -0.132,0.143 -0.115,0.293 -0.115,0.318v1.541l0.021,-0.125c-0.035,0.1 -0.035,0.208 0,0.309l0.029,0.084 0.797,0.773 0.004,0.004c0.195,0.18 0.502,0.18 0.697,0l0.004,-0.002 0.824,-0.791 0.025,-0.111c0.016,-0.073 0.016,-0.148 0,-0.221l0.008,0.08v-1.543,-0.004c-0.003,-0.185 -0.115,-0.354 -0.285,-0.428l-0.072,-0.031zM6.316,11.085h0.91v1.156l-0.459,0.439 -0.451,-0.439z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m42.812,19.171c-1.007,0 -1.83,0.823 -1.83,1.83 0,1.007 0.823,1.83 1.83,1.83 1.007,0 1.83,-0.823 1.83,-1.83 0,-1.007 -0.823,-1.83 -1.83,-1.83zM42.812,19.77c0.683,0 1.229,0.548 1.229,1.23 0,0.683 -0.546,1.229 -1.229,1.229 -0.683,0 -1.23,-0.546 -1.23,-1.229 0,-0.683 0.548,-1.23 1.23,-1.23z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m8.71,19.171c-1.007,0 -1.83,0.823 -1.83,1.83 0,1.007 0.823,1.83 1.83,1.83 1.007,0 1.83,-0.823 1.83,-1.83 0,-1.007 -0.823,-1.83 -1.83,-1.83zM8.71,19.77c0.683,0 1.23,0.548 1.23,1.23 0,0.683 -0.548,1.229 -1.23,1.229 -0.683,0 -1.229,-0.546 -1.229,-1.229 0,-0.683 0.546,-1.23 1.229,-1.23z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m43.441,14.02a1.05,1.05 0,0 1,-1.05 1.05,1.05 1.05,0 0,1 -1.05,-1.05 1.05,1.05 0,0 1,1.05 -1.05,1.05 1.05,0 0,1 1.05,1.05z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m42.392,12.595c-0.783,0 -1.426,0.643 -1.426,1.426 0,0.783 0.643,1.424 1.426,1.424 0.783,0 1.424,-0.641 1.424,-1.424 0,-0.783 -0.641,-1.426 -1.424,-1.426zM42.392,13.345c0.377,0 0.674,0.299 0.674,0.676 0,0.377 -0.297,0.674 -0.674,0.674 -0.377,0 -0.676,-0.297 -0.676,-0.674 0,-0.377 0.299,-0.676 0.676,-0.676z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m45.941,16.52a1.05,1.05 0,0 1,-1.05 1.05,1.05 1.05,0 0,1 -1.05,-1.05 1.05,1.05 0,0 1,1.05 -1.05,1.05 1.05,0 0,1 1.05,1.05z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m44.892,15.095c-0.783,0 -1.426,0.643 -1.426,1.426 0,0.783 0.643,1.424 1.426,1.424 0.783,0 1.424,-0.641 1.424,-1.424 0,-0.783 -0.641,-1.426 -1.424,-1.426zM44.892,15.845c0.377,0 0.674,0.299 0.674,0.676 0,0.377 -0.297,0.674 -0.674,0.674 -0.377,0 -0.676,-0.297 -0.676,-0.674 0,-0.377 0.299,-0.676 0.676,-0.676z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m48.441,14.02a1.05,1.05 0,0 1,-1.05 1.05,1.05 1.05,0 0,1 -1.05,-1.05 1.05,1.05 0,0 1,1.05 -1.05,1.05 1.05,0 0,1 1.05,1.05z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m47.392,12.595c-0.783,0 -1.426,0.643 -1.426,1.426 0,0.783 0.643,1.424 1.426,1.424 0.783,0 1.424,-0.641 1.424,-1.424 0,-0.783 -0.641,-1.426 -1.424,-1.426zM47.392,13.345c0.377,0 0.674,0.299 0.674,0.676 0,0.377 -0.297,0.674 -0.674,0.674 -0.377,0 -0.676,-0.297 -0.676,-0.674 0,-0.377 0.299,-0.676 0.676,-0.676z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m45.941,11.52a1.05,1.05 0,0 1,-1.05 1.05,1.05 1.05,0 0,1 -1.05,-1.05 1.05,1.05 0,0 1,1.05 -1.05,1.05 1.05,0 0,1 1.05,1.05z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m44.892,10.095c-0.783,0 -1.426,0.643 -1.426,1.426 0,0.783 0.643,1.424 1.426,1.424 0.783,0 1.424,-0.641 1.424,-1.424 0,-0.783 -0.641,-1.426 -1.424,-1.426zM44.892,10.845c0.377,0 0.674,0.299 0.674,0.676 0,0.377 -0.297,0.674 -0.674,0.674 -0.377,0 -0.676,-0.297 -0.676,-0.674 0,-0.377 0.299,-0.676 0.676,-0.676z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m10.99,7.32v0.5,16.26h29.531L40.521,7.32ZM11.99,8.32h27.531v14.76h-27.531z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="M12.201,3.5C5.454,3.5 0,8.952 0,15.699v0.012c0,6.748 5.454,12.199 12.201,12.199h27.109c6.748,0 12.201,-5.452 12.201,-12.199v-0.012C51.512,8.952 46.058,3.5 39.311,3.5ZM12.201,4.801h27.109c6.05,0 10.9,4.849 10.9,10.898v0.012c0,6.05 -4.851,10.898 -10.9,10.898L12.201,26.609c-6.05,0 -10.9,-4.849 -10.9,-10.898v-0.012c0,-6.05 4.851,-10.898 10.9,-10.898z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m5.637,24.529 l-0.012,1 4.623,0.051h0.309c-0.109,-0.014 0.023,0.022 0.158,0.186 0.138,0.167 0.298,0.413 0.441,0.66 0.287,0.495 0.508,0.988 0.508,0.988l0.912,-0.408c0,0 -0.236,-0.532 -0.555,-1.082 -0.159,-0.275 -0.337,-0.555 -0.535,-0.795 -0.198,-0.24 -0.383,-0.488 -0.797,-0.545l-0.035,-0.004h-0.4z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m11.664,4.025c0,0 -0.221,0.493 -0.508,0.988 -0.143,0.247 -0.303,0.495 -0.441,0.662 -0.135,0.163 -0.268,0.2 -0.158,0.186 -0.088,0.006 -0.176,0.007 -0.264,0l-0.023,-0.002 -4.645,0.051 0.012,1 4.619,-0.051 -0.047,-0.002c0.151,0.013 0.302,0.013 0.453,0h0.014l0.014,-0.002c0.414,-0.057 0.599,-0.303 0.797,-0.543 0.198,-0.24 0.376,-0.522 0.535,-0.797 0.318,-0.55 0.555,-1.08 0.555,-1.08z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m45.756,24.529 l-4.627,0.051h-0.393l-0.033,0.004c-0.412,0.056 -0.602,0.305 -0.801,0.545 -0.199,0.24 -0.376,0.522 -0.535,0.797 -0.318,0.551 -0.553,1.082 -0.553,1.082l0.914,0.404c0,0 0.219,-0.492 0.504,-0.986 0.142,-0.247 0.302,-0.494 0.439,-0.66 0.135,-0.163 0.266,-0.199 0.162,-0.186h0.299l4.633,-0.051z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
<path
android:pathData="m39.729,4.027 l-0.914,0.406c0,0 0.235,0.531 0.553,1.082 0.159,0.275 0.336,0.557 0.535,0.797 0.199,0.24 0.388,0.487 0.801,0.543l0.012,0.002h0.014c0.148,0.013 0.297,0.013 0.445,0l-0.049,0.002 4.631,0.051 0.01,-1 -4.654,-0.051 -0.023,0.002c-0.085,0.007 -0.169,0.006 -0.254,0 0.104,0.013 -0.027,-0.023 -0.162,-0.186C40.534,5.509 40.375,5.263 40.232,5.016 39.947,4.521 39.729,4.027 39.729,4.027Z"
android:fillColor="#2f2f2f"
android:strokeColor="#00000000"/>
</vector>
+4
View File
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M128,24A104,104 0,1 0,232 128,104.11 104.11,0 0,0 128,24ZM128,216a88,88 0,1 1,88 -88A88.1,88.1 0,0 1,128 216ZM144,176a8,8 0,0 1,-8 8,16 16,0 0,1 -16,-16L120,128a8,8 0,0 1,0 -16,16 16,0 0,1 16,16v40A8,8 0,0 1,144 176ZM112,84a12,12 0,1 1,12 12A12,12 0,0 1,112 84Z"
android:fillColor="#000000"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector android:height="24dp" android:viewportHeight="425.72"
android:viewportWidth="425.72" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000"
android:pathData="m70.82,87.19c-6.6,1.9 -7.7,2.7 -9.1,6.9 -0.5,1.8 -2.3,4 -4,4.9 -4.3,2.6 -5.6,5.9 -7,17.9 -1.2,9.7 -1.5,10.7 -3.8,11.9 -4.9,2.7 -15,14.9 -19.5,23.7 -7.3,14.2 -9.7,23.8 -17.6,71.7 -8.9,53.3 -10.3,63.7 -9.7,71.5 1.1,16.1 9.2,31.1 20,37.5 13.7,8 31.4,9.1 44.9,2.7 10.9,-5.1 14.4,-9.5 36.1,-45l11.8,-19.3 5.5,6.1c3,3.3 7.5,7.2 9.9,8.6 22,12.9 50.7,5.2 62.3,-16.6l3.2,-6l20.4,-0 20.5,-0l2.6,5.1c3.2,6.4 10.4,14.2 16.2,17.6 17.9,10.5 41.6,7.3 55.3,-7.5 2.2,-2.3 4.1,-4.2 4.3,-4.2 0.2,-0 7,10.3 15,22.8 8,12.6 16.2,24.7 18.2,27 8,9.4 18.8,14.2 32,14.2 24.4,-0 39.7,-11.3 45.4,-33.6 1.1,-4.2 2,-9.7 2,-12.1 0,-9.3 -14.7,-106.4 -18.5,-122.1 -0.9,-3.7 -3.7,-11 -6.2,-16.2 -3.7,-7.7 -6,-10.8 -12.2,-17 -4.2,-4.2 -8.6,-8.3 -9.9,-9.1 -2,-1.2 -2.5,-2.8 -3.8,-12.9 -1.7,-12.7 -2.6,-14.8 -8.3,-18.6 -2.2,-1.4 -4.2,-3.6 -4.5,-4.8 -1,-4.1 -10.3,-6.6 -24.6,-6.6 -14,-0 -23.4,2.5 -24.6,6.4 -0.4,1.1 -2.6,3.8 -5,5.9 -4.7,4.1 -5.7,6.6 -7.4,17.7 -0.6,4.1 -1.3,8.5 -1.6,9.7l-0.4,2.3l-36.1,-0c-31.7,-0 -36.4,0.2 -39.2,1.7 -4.4,2.2 -16.9,2.3 -22.1,-0 -5.2,-2.3 -18.6,-2.4 -18.6,-0.2 0,0.8 -0.9,1.5 -2,1.5 -1.1,-0 -2,-0.7 -2,-1.5 0,-1.1 -1.2,-1.5 -4.5,-1.5 -3.3,-0 -4.5,0.4 -4.5,1.5 0,0.8 -0.7,1.5 -1.5,1.5 -0.8,-0 -1.5,-0.7 -1.5,-1.5 0,-1.1 -1.2,-1.5 -4.4,-1.5 -3,-0 -4.6,0.5 -5,1.5 -0.3,0.8 -1.2,1.5 -2.1,1.5 -0.8,-0 -1.5,-0.7 -1.5,-1.5 0,-1.1 -1.2,-1.5 -4.5,-1.5 -3.3,-0 -4.5,0.4 -4.5,1.5 0,0.8 -0.9,1.5 -2,1.5 -1.1,-0 -2,-0.7 -2,-1.5 0,-1.2 -1.4,-1.5 -6.5,-1.5 -7.4,-0 -6.6,1.3 -8.9,-13.6 -1.6,-10.1 -3.2,-13.3 -8.1,-16.4 -1.8,-1.1 -4.2,-3.5 -5.3,-5.3 -2.6,-4.6 -7.3,-5.9 -22.7,-6.3 -9.5,-0.3 -14.4,-0 -18.4,1.1zM102.72,122.29c3.6,0.8 10.3,3.3 15,5.6 11.9,5.7 23.8,17.7 29.8,29.9 6.7,13.6 8.8,28.2 6.1,43.6 -0.9,5.3 -1.4,6.3 -3.1,6.3 -1.7,-0 -1.9,-0.4 -1.3,-2.3 1.6,-5.2 1.7,-25.1 0.1,-31.9 -5.4,-23 -24.1,-41.9 -46.9,-47.3 -8,-1.9 -22.5,-1.9 -30.6,-0 -21.9,5.2 -41.4,24.7 -46.6,46.6 -1.9,8.2 -1.9,22.6 0.1,30.9 4.1,17.5 17.1,33.8 33.4,41.9 13.9,6.8 25.7,8.5 40.6,5.6 6.2,-1.2 8,-1.2 8.6,-0.2 1.5,2.4 -6.9,4.1 -20.7,4.1 -11.1,0.1 -14,-0.2 -20,-2.2 -17.5,-5.8 -32.6,-18.6 -40.3,-34.3 -9.6,-19.5 -9.7,-40.9 -0.1,-60.7 5.9,-12.2 17.9,-24.2 29.9,-30 4.7,-2.3 11.2,-4.8 14.5,-5.6 8,-1.9 23.4,-1.9 31.5,-0zM355.12,122.19c22,4.7 42.2,23.1 49.2,44.9 12.3,38.2 -11.3,78.8 -50.7,87.2 -5.2,1.1 -10.5,1.5 -17.9,1.1 -11.4,-0.5 -16,-1.8 -16,-4.3 0,-1.1 0.5,-1.4 1.6,-1 4.1,1.6 16.6,2.8 22.9,2.3 26.5,-2.3 48.8,-20.2 56.7,-45.7 3.4,-11 3.2,-26.4 -0.6,-37.7 -14.8,-45.3 -72.1,-59.1 -105.6,-25.4 -15.4,15.5 -22,37.1 -17.6,57.8 1.2,5.8 1.1,6.3 -0.4,6.3 -4.1,-0 -5.4,-23.3 -2,-36.4 3.1,-12.2 8.2,-20.9 18,-30.6 9.6,-9.7 18.3,-14.9 30.2,-18 8.9,-2.3 22.6,-2.5 32.2,-0.5zM347.42,141.79c13.7,6.6 8.8,27 -6.5,27.2 -8.5,0.1 -14.2,-5.6 -14.2,-14.4 0,-5.6 2.9,-10.4 8,-13 4.5,-2.4 7.4,-2.4 12.7,0.2zM89.72,146.19 L92.02,148.69l-5.9,-0 -5.9,-0l3,-2.5c3.7,-3 3.7,-3 6.5,-0zM96.52,164.09c0.3,7.1 0.1,8.1 -2,10.3 -2.4,2.6 -6.9,5.3 -8.7,5.3 -0.6,-0 -2.9,-1.6 -5.1,-3.6l-4,-3.6l0,-7.7c0,-4.3 0.3,-8.1 0.7,-8.5 0.4,-0.4 4.8,-0.5 9.8,-0.4l9,0.3zM381.62,174.49c10,5 10.6,18.6 1.2,24.9 -9.2,6.2 -22.1,-1 -22.1,-12.2 0,-10.7 11.3,-17.6 20.9,-12.7zM311.82,175.99c9.4,6.3 8.8,19.9 -1.2,24.9 -9.6,4.9 -20.9,-2 -20.9,-12.7 0,-11.2 12.9,-18.4 22.1,-12.2zM72.42,181.89c6,6.5 6,8.2 0,14.5 -2.9,3.1 -3.5,3.3 -9.8,3.3 -9,-0 -9.9,-1 -9.9,-10.5 0,-9.5 0.9,-10.5 9.9,-10.5 6.3,-0 6.9,0.2 9.8,3.2zM117.82,179.69c1.6,0.9 1.9,2.2 1.9,9.3 0,5.1 -0.5,8.8 -1.2,9.5 -0.7,0.7 -4,1.2 -7.9,1.2 -7.5,-0 -10.2,-1.5 -13.4,-7.3 -1.8,-3.4 -1.8,-3.5 0.5,-7.2 1.3,-2 3.7,-4.3 5.4,-5.1 3.7,-1.6 12,-1.8 14.7,-0.4zM188.52,182.29c2.4,1.7 3,7 1.2,10.5 -0.9,1.6 -2.2,1.9 -9.3,1.9 -5.1,-0 -8.8,-0.5 -9.5,-1.2 -1.9,-1.9 -1.5,-10.5 0.6,-11.7 2.8,-1.7 14.4,-1.3 17,0.5zM255.62,185.09c3.8,1.7 4.9,4.2 2.9,6.2 -2.3,2.4 -17,4.5 -20.1,3 -3.2,-1.7 -3.7,-9.8 -0.7,-11.9 1.9,-1.3 2.9,-1.3 8.8,-0 3.7,0.8 7.8,2 9.1,2.7zM131.42,191.49 L128.72,194.19l0,-5.8 0,-5.9l2.7,3.2 2.6,3.2zM45.72,190.29c0,5 -0.2,5.5 -1.6,4.4 -3.3,-2.8 -3.7,-5 -1.3,-7.6 1.2,-1.3 2.4,-2.4 2.6,-2.4 0.2,-0 0.3,2.5 0.3,5.6zM92.82,202.59c3.8,3.7 3.9,4.1 3.9,10.8 0,4 -0.5,7.4 -1.2,8.1 -0.7,0.7 -4.2,1.2 -8.8,1.2 -4.6,-0 -8.1,-0.5 -8.8,-1.2 -0.7,-0.7 -1.2,-4.1 -1.2,-8.4 0,-7.1 0,-7.2 4,-10.8 2.2,-2 5,-3.6 6.1,-3.6 1.2,-0 3.9,1.7 6,3.9zM221.42,204.79c12.1,7.5 6.5,26.9 -7.7,26.9 -5,-0 -10.7,-3.8 -13,-8.5 -2.5,-5.1 -2.5,-6.9 0,-12.2 3.5,-7.9 13.3,-10.8 20.7,-6.2zM349.22,209.09c10.6,8.1 4.4,25.6 -9,25.6 -7.8,-0 -13.6,-5.4 -14.3,-13.2 -0.3,-4.4 0,-5.9 2,-8.8 4.9,-7.3 14.3,-8.9 21.3,-3.6zM163.72,220.29c6.8,3.1 12.3,8.3 15.8,14.9 2.3,4.3 2.7,6.2 2.7,13.5 0,7.3 -0.4,9.2 -2.7,13.6 -7.2,13.8 -23.8,21.1 -38.4,17 -20.1,-5.8 -30.1,-26.6 -21.1,-44.2 8.1,-15.7 27.2,-22.2 43.7,-14.8zM292.32,220.19c20.8,9.4 25.7,36.2 9.2,51 -6.8,6.2 -14,8.9 -23.3,8.9 -9.9,-0 -16.7,-2.8 -23.7,-9.7 -8.1,-8.2 -11,-21 -7.2,-32.1 2,-5.8 10,-14.7 15.8,-17.6 9.3,-4.5 20,-4.7 29.2,-0.5zM90.72,232.29c-2.7,3.2 -4.6,3.4 -7,0.9l-2.3,-2.5l5.2,-0c4.7,-0 5.2,0.2 4.1,1.6z" android:strokeColor="#00000000"/>
<path android:fillColor="#000000"
android:pathData="m138.22,225.79c-5.1,2.5 -10.1,7.4 -12.8,12.4 -1.7,3.1 -2.2,5.8 -2.2,11.5 0,8.7 2,13.2 8.2,19.2 5.1,4.9 10.5,7 17.9,7 10.3,0.1 18.7,-5.1 23.7,-14.6 3,-5.8 3.1,-17.4 0,-23.1 -2.8,-5.2 -8.8,-11 -13.5,-13 -5.4,-2.2 -16.3,-1.9 -21.3,0.6z" android:strokeColor="#00000000"/>
<path android:fillColor="#000000"
android:pathData="m268.12,225.59c-15.5,7.1 -20.2,27.5 -9.4,41.1 6.8,8.6 21.4,11.9 31.3,7.1 8.8,-4.3 15.7,-14.9 15.7,-24.1 0,-9.2 -6.8,-19.6 -15.4,-23.6 -6.5,-3 -16.2,-3.2 -22.2,-0.5z" android:strokeColor="#00000000"/>
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="29.424"
android:viewportHeight="29.424">
<path
android:fillColor="#FF000000"
android:pathData="m14.712,-0c-3.803,0 -7.607,0.701 -10.992,2.103 -0.704,0.292 -1.325,0.913 -1.617,1.617 -2.804,6.77 -2.804,15.214 0,21.984 0.292,0.704 0.913,1.325 1.617,1.617 6.77,2.804 15.214,2.804 21.984,0 0.704,-0.292 1.325,-0.913 1.617,-1.617 2.804,-6.77 2.804,-15.214 0,-21.984 -0.292,-0.704 -0.913,-1.325 -1.617,-1.617 -3.385,-1.402 -7.188,-2.103 -10.992,-2.103zM20.15,5.003c1.807,3.129 3.612,6.258 5.419,9.387l-10.839,0c1.806,-3.129 3.613,-6.258 5.419,-9.387zM5.277,5.652l8.719,0l0,8.719L5.277,14.371ZM6.678,7.052l0,5.918l5.918,0L12.596,7.052ZM20.15,7.869c-0.979,1.696 -1.959,3.392 -2.938,5.088l5.876,0c-0.979,-1.696 -1.959,-3.392 -2.938,-5.088zM20.14,14.938c0.039,-0 0.077,0 0.116,0.001 0.364,-0 0.727,0.041 1.082,0.123 2.378,0.501 4.09,3.008 3.664,5.406 -0.324,2.452 -2.777,4.359 -5.235,4.047 -2.465,-0.199 -4.49,-2.554 -4.305,-5.022 0.071,-2.433 2.252,-4.544 4.679,-4.555zM12.904,15.595c0.34,0.336 0.68,0.672 1.02,1.008 -1.085,1.098 -2.169,2.195 -3.254,3.293 1.097,1.085 2.194,2.169 3.291,3.254 -0.335,0.34 -0.671,0.68 -1.006,1.02l-3.293,-3.256 -3.256,3.293c-0.34,-0.336 -0.68,-0.672 -1.02,-1.008l3.256,-3.293c-1.098,-1.085 -2.195,-2.169 -3.293,-3.254 0.336,-0.34 0.672,-0.68 1.008,-1.02l3.293,3.254c1.085,-1.097 2.169,-2.194 3.254,-3.291zM20.347,16.447c-0.03,0 -0.061,0.001 -0.091,0.002 -1.242,-0.016 -2.443,0.735 -2.969,1.86 -0.891,1.722 0.112,4.06 1.987,4.576 1.838,0.634 4.008,-0.707 4.238,-2.642 0.356,-1.888 -1.252,-3.803 -3.164,-3.796z"
android:strokeWidth="1"
android:strokeLineCap="square"/>
</vector>
+6
View File
@@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="426.81"
android:viewportWidth="426.81" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000"
android:pathData="m73.19,122.41 l-14.23,14h-28.7,-4.43c-6.4,7.57 -10.74,14.82 -14.96,24.9 -12,28.5 -13.7,60.5 -4.5,88.3 5.4,16.2 20.4,39.7 32.2,50.3l5,4.5L196.27,304.4 348.87,304.4l9,-9 8.9,-9 17,-0.2 17,-0.3 3.8,-5c9.1,-11.9 17.3,-32 20.2,-49.4 5.55,-33.2 -4.23,-71.49 -23.84,-95.5L391.7,136l-9.93,0.2c-8.1,0.1 -15.3,0.1 -15.9,-0.2 -0.57,-0.19 -2.84,-2.48 -5.02,-5.08l-8.42,-8.52L213.47,122.41ZM87.37,133.4h124.5,124.5v72,72h-124.5,-124.5v-72zM381.24,162.24c6.86,0 12.42,5.56 12.42,12.42 0,6.86 -5.56,12.42 -12.42,12.42 -6.86,0 -12.42,-5.56 -12.42,-12.42 0,-6.86 5.56,-12.42 12.42,-12.42zM36.78,167.09h16.94c0.75,0 1.35,0.61 1.35,1.35v15.7c0,0.87 -0.35,1.69 -0.98,2.29l-7.89,7.58c-0.55,0.52 -1.41,0.52 -1.96,0l-7.91,-7.58c-0.62,-0.6 -0.98,-1.43 -0.97,-2.29l0.05,-15.7c0,-0.75 0.61,-1.36 1.36,-1.36zM355.46,188.03c6.86,0 12.42,5.56 12.42,12.42 0,6.86 -5.56,12.42 -12.42,12.42 -6.86,0 -12.42,-5.56 -12.42,-12.42 0,-6.86 5.56,-12.42 12.42,-12.42zM407.02,188.03c6.86,0 12.42,5.56 12.42,12.42 0,6.86 -5.56,12.42 -12.42,12.42 -6.86,0 -12.42,-5.56 -12.42,-12.42 0,-6.86 5.56,-12.42 12.42,-12.42zM29.38,190.15c0.87,-0 1.69,0.35 2.29,0.97l7.58,7.91c0.52,0.55 0.52,1.41 -0,1.96l-7.58,7.89c-0.6,0.62 -1.43,0.98 -2.29,0.98h-15.7c-0.75,0 -1.35,-0.61 -1.35,-1.35v-16.94c0,-0.75 0.61,-1.36 1.35,-1.36zM61.25,190.33h15.7c0.75,0 1.35,0.61 1.35,1.35v16.94c0,0.75 -0.61,1.36 -1.36,1.36l-15.7,0.05c-0.87,0 -1.7,-0.35 -2.29,-0.97l-7.58,-7.91c-0.52,-0.55 -0.52,-1.41 0,-1.96l7.58,-7.89c0.6,-0.62 1.43,-0.98 2.3,-0.98zM45.39,205.76c0.35,-0 0.71,0.13 0.98,0.39l7.91,7.58c0.62,0.6 0.98,1.43 0.98,2.29l-0.05,15.7c-0,0.75 -0.61,1.36 -1.36,1.36h-16.94c-0.75,0 -1.35,-0.61 -1.35,-1.35v-15.7c0,-0.87 0.35,-1.69 0.98,-2.29l7.89,-7.58c0.27,-0.26 0.63,-0.39 0.98,-0.39zM381.24,213.81c6.86,0 12.42,5.56 12.42,12.42 0,6.86 -5.56,12.42 -12.42,12.42 -6.86,0 -12.42,-5.56 -12.42,-12.42 0,-6.86 5.56,-12.42 12.42,-12.42zM48.04,243.19a14.41,14.41 0,0 1,14.41 14.41,14.41 14.41,0 0,1 -14.41,14.41 14.41,14.41 0,0 1,-14.41 -14.41,14.41 14.41,0 0,1 6.47,-11.69c0.3,-0.22 0.61,-0.43 0.93,-0.62a14.41,14.41 0,0 1,2.57 -1.21c1.17,-0.41 2.39,-0.66 3.65,-0.73a14.41,14.41 0,0 1,0.8 -0.16zM36.97,289.26h25.36l11.85,10.16h-25.36l-0.01,-0.01h-2.03l-4.9,-5 -4.9,-5h0.17z"
android:strokeColor="#00000000" android:strokeWidth="0.0999997"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:fillColor="#FF000000"
android:pathData="M19.313,4C19.012,4 18.707,3.988 18.406,4.188C18.105,4.387 18,4.699 18,5L18,41.594C18,41.992 18.289,42.395 18.688,42.594L26.688,45L27,45C27.199,45 27.395,44.914 27.594,44.813C27.895,44.613 28,44.301 28,44L28,13.406C28.602,13.707 29,14.301 29,15L29,26.094C29,26.395 29.199,26.805 29.5,26.906C29.699,27.008 31.199,27.906 34,27.906C36.699,27.906 40,26.414 40,19.313C40,13.613 36.813,9.293 31.313,7.594ZM17,26.406L5.906,30.406L4.313,31C1.613,32.102 0,33.887 0,35.688C0,39.488 2.699,41.688 7.5,41.688C10.102,41.688 13.301,41.113 17,39.813L17,36C16.102,36.301 15.113,36.699 14.313,37C12.711,37.602 11.5,37.813 10.5,37.813C9,37.813 8.301,37.301 8,37C7.602,36.699 7.398,36.313 7.5,35.813C7.602,34.813 8.801,33.895 11,33.094C11.5,32.895 14.898,31.699 17,31ZM36.5,28.906C34.102,29.008 31.602,29.395 29,30.094L29,34.688C30.102,34.289 31.586,33.801 33.688,33C38.488,31.301 40.492,31.488 41.094,31.688C42.293,31.789 42.801,32.5 43,33C43.5,34.5 41.613,35.188 38.813,36.188C37.512,36.688 31.898,38.688 29,39.688L29,44.313L44.5,38.813L45.688,38.313C47.688,37.613 50.199,36.301 50,34C49.898,31.801 47.211,30.695 45.313,30.094C42.512,29.195 39.5,28.805 36.5,28.906Z"/>
</vector>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<gradient
android:startColor="#FF00A7FF"
android:endColor="#FF93278F"
android:angle="270"/>
</shape>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="67.733dp"
android:height="67.733dp"
android:viewportWidth="67.733"
android:viewportHeight="67.733">
<path
android:fillColor="#FF000000"
android:pathData="M56.541,21.775 L41.725,6.959c-0.403,-0.393 -0.945,-0.612 -1.508,-0.609L14.817,6.35c-2.338,0 -4.233,1.895 -4.233,4.233v23.283c-0,1.169 0.948,2.117 2.117,2.117h42.333c1.169,0 2.117,-0.948 2.117,-2.117v-10.583c0.003,-0.563 -0.215,-1.105 -0.609,-1.508zM40.217,23.283v-11.642l11.642,11.642zM24.342,44.45c0,1.169 -0.948,2.117 -2.117,2.117h-2.646v10.583c0,2.822 -4.233,2.822 -4.233,0v-10.583h-2.646c-2.818,-0.004 -2.818,-4.229 0,-4.233h9.525c1.169,0 2.117,0.948 2.117,2.117zM40.217,53.975c0,3.466 -2.461,5.556 -6.615,5.556 -1.921,-0.006 -3.788,-0.637 -5.318,-1.799v0c-0.501,-0.394 -0.786,-1.003 -0.767,-1.64 -0,-1.169 0.948,-2.117 2.117,-2.117 0.49,0.004 0.965,0.171 1.349,0.476h-0.026c0.781,0.533 1.7,0.827 2.646,0.847 2.381,0 2.381,-0.794 2.381,-1.323 0,-0.529 0,-0.238 -0.317,-0.423 -0.317,-0.185 -1.535,-0.582 -2.434,-0.82 -2.275,-0.609 -5.715,-1.535 -5.715,-5.106 0,-3.572 2.514,-5.556 6.085,-5.556 1.921,0.006 3.788,0.637 5.318,1.799v0c0.501,0.394 0.786,1.003 0.767,1.64 0,1.169 -0.948,2.117 -2.117,2.117 -0.49,-0.004 -0.965,-0.171 -1.349,-0.476h0.026c-0.781,-0.533 -1.7,-0.827 -2.646,-0.847 -1.217,0 -1.852,0.45 -1.852,1.323v0.053c0.291,0.344 1.746,0.741 2.566,0.952 2.196,0.582 5.9,1.561 5.9,5.345zM55.798,42.855c-1.081,-0.457 -2.328,0.054 -2.778,1.138l-3.334,8.017 -3.334,-8.017c-1.095,-2.606 -5.005,-0.968 -3.916,1.64l5.292,12.7c0.719,1.75 3.197,1.75 3.916,0l2.864,-6.874 2.427,-5.826c0.457,-1.081 -0.054,-2.328 -1.138,-2.778z"
android:strokeWidth="0.264583"/>
</vector>
+8
View File
@@ -0,0 +1,8 @@
<vector android:height="24dp" android:viewportHeight="115.43"
android:viewportWidth="115.43" android:width="24dp"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:fillType="nonZero"
android:pathData="m27.32,30.4c-15.13,0 -27.32,12.18 -27.32,27.32 0,15.13 12.18,27.32 27.32,27.32l60.79,0c15.13,0 27.32,-12.18 27.32,-27.32 0,-15.13 -12.18,-27.32 -27.32,-27.32zM24.62,38.44L90.61,38.44L90.61,76.22L24.62,76.22ZM10.54,44.52l4.03,0c0.18,0 0.32,0.14 0.32,0.32l0,3.74c0,0.21 -0.08,0.4 -0.23,0.55l-1.88,1.8c-0.13,0.12 -0.34,0.12 -0.47,0l-1.88,-1.8c-0.15,-0.14 -0.23,-0.34 -0.23,-0.55l0.01,-3.74c0,-0.18 0.15,-0.32 0.32,-0.32zM104.06,44.6c1.54,0 2.78,1.25 2.78,2.78 0,1.54 -1.25,2.78 -2.78,2.78 -1.54,0 -2.78,-1.25 -2.78,-2.78 0,-1.54 1.25,-2.78 2.78,-2.78zM109.1,49.65c1.54,0 2.78,1.25 2.78,2.78 0,1.54 -1.25,2.78 -2.78,2.78 -1.54,0 -2.78,-1.25 -2.78,-2.78 0,-1.54 1.25,-2.78 2.78,-2.78zM99.01,49.65c1.54,0 2.78,1.25 2.78,2.78 0,1.54 -1.25,2.78 -2.78,2.78 -1.54,0 -2.78,-1.25 -2.78,-2.78 0,-1.54 1.25,-2.78 2.78,-2.78zM8.78,50c0.21,-0 0.4,0.08 0.55,0.23l1.81,1.88c0.12,0.13 0.12,0.34 -0,0.47l-1.8,1.88c-0.14,0.15 -0.34,0.23 -0.55,0.23l-3.74,0c-0.18,0 -0.32,-0.14 -0.32,-0.32l0,-4.03c0,-0.18 0.14,-0.32 0.32,-0.32zM16.37,50.05l3.74,0c0.18,0 0.32,0.14 0.32,0.32l0,4.03c0,0.18 -0.14,0.32 -0.32,0.32l-3.74,0.01c-0.21,0 -0.4,-0.08 -0.55,-0.23l-1.8,-1.88c-0.12,-0.13 -0.12,-0.34 0,-0.47l1.8,-1.88c0.14,-0.15 0.34,-0.23 0.55,-0.23zM12.59,53.72c0.08,-0 0.17,0.03 0.23,0.09l1.88,1.81c0.15,0.14 0.23,0.34 0.23,0.55l-0.01,3.74c-0,0.18 -0.15,0.32 -0.32,0.32l-4.03,0c-0.18,0 -0.32,-0.14 -0.32,-0.32l0,-3.74c0,-0.21 0.08,-0.4 0.23,-0.55l1.88,-1.8c0.06,-0.06 0.15,-0.09 0.23,-0.09zM104.06,54.69c1.54,0 2.78,1.25 2.78,2.78 0,1.54 -1.25,2.78 -2.78,2.78 -1.54,0 -2.78,-1.25 -2.78,-2.78 0,-1.54 1.25,-2.78 2.78,-2.78zM15.83,66.39a2.87,2.87 0,0 1,2.87 2.87,2.87 2.87,0 0,1 -2.87,2.87 2.87,2.87 0,0 1,-2.87 -2.87,2.87 2.87,0 0,1 2.87,-2.87zM99.39,66.41a2.87,2.87 89.99,0 1,2.87 2.87,2.87 2.87,89.99 0,1 -2.87,2.87 2.87,2.87 89.99,0 1,-2.87 -2.87,2.87 2.87,89.99 0,1 2.87,-2.87z"
android:strokeColor="#00000000" android:strokeLineCap="square"
android:strokeLineJoin="round" android:strokeWidth="1"/>
</vector>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-31
View File
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<include
android:id="@+id/include"
layout="@layout/appbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="56dp"
android:background="@color/colorAmoled"
android:backgroundTint="@color/colorAmoled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include"
tools:context=".activities.MainActivity" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
This layout is used only on API 21 & 22 (Lollipop) because on these API levels,
Android doesn't scale SVGs very well - we noticed the logo stretched across the
screen and looked ugly.
In SplashActivity, setContentView(R.layout.activity_splash) is called only for API 21 & 22.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/splashRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:srcCompat="@drawable/splash" />
</LinearLayout>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="afterDescendants"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorAmoled"
android:theme="@style/NPSTheme.ActionBar"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</com.google.android.material.appbar.AppBarLayout>
-39
View File
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
tools:context=".fragments.mainactivity.MainActivityFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/appsRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:listitem="@layout/recycler_item_grid" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/tsvChooserButton"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginBottom="40dp"
android:backgroundTint="@color/colorPrimary"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_add_24dp"
app:borderWidth="2dp"
app:fabCustomSize="52dp"
app:layout_anchor="@+id/appsRecycler"
app:layout_anchorGravity="bottom|center"
app:maxImageSize="32dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -1,116 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="0dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="214dp"
android:backgroundTint="@color/colorAmoled"
app:cardBackgroundColor="@color/colorPrimary"
app:cardElevation="4dp"
app:cardUseCompatPadding="false"
app:contentPadding="15dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView2"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_games_placeholder_24dp" />
<TextView
android:id="@+id/appTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="marquee"
android:enabled="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="@color/colorOnPrimary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView2"
tools:text="App Title" />
<TextView
android:id="@+id/appRegion"
android:layout_width="36dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/rect_light_blue"
android:paddingStart="4dp"
android:paddingLeft="10dp"
android:paddingTop="4dp"
android:paddingEnd="4dp"
android:paddingRight="10dp"
android:paddingBottom="4dp"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="ASIA" />
<TextView
android:id="@+id/appMinFW"
android:layout_width="36dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginBottom="8dp"
android:background="@drawable/rect_red"
android:padding="4dp"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/appRegion"
tools:text="3.69" />
<TextView
android:id="@+id/appLastDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:padding="4dp"
android:textColor="@color/colorOnSecondary"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="2018-04-22" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="0dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/colorAmoled"
app:cardBackgroundColor="@color/colorPrimary"
app:cardElevation="4dp"
app:cardUseCompatPadding="false"
app:contentPadding="15dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="62dp"
android:layout_height="56dp"
android:contentDescription="app icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_games_placeholder_24dp" />
<TextView
android:id="@+id/appTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:ellipsize="marquee"
android:focusable="auto"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="@color/colorOnPrimary"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="App Title" />
<TextView
android:id="@+id/appRegion"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:background="@drawable/rect_light_blue"
android:paddingStart="4dp"
android:paddingLeft="10dp"
android:paddingTop="4dp"
android:paddingEnd="4dp"
android:paddingRight="10dp"
android:paddingBottom="4dp"
android:textAlignment="center"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/appMinFW"
app:layout_constraintStart_toEndOf="@+id/imageView"
tools:text="ASIA" />
<TextView
android:id="@+id/appMinFW"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/rect_red"
android:padding="4dp"
android:textAlignment="center"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/appRegion"
tools:text="3.69" />
<TextView
android:id="@+id/appLastDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:textColor="@color/colorOnSecondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="2020-02-12" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/prefers"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<FrameLayout
android:id="@+id/prefs"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -13,7 +13,7 @@
android:fontFamily="sans-serif-light"
android:gravity="center"
android:textAlignment="center"
android:textColor="@color/colorAccent"
android:textColor="@color/colorError"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
@@ -70,19 +70,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/singleAppTitle" />
<Button
android:id="@+id/singleAppCopyPKGButton"
android:layout_width="0dp"
android:layout_height="66dp"
android:layout_marginStart="48dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="48dp"
android:text="@string/copy_pkg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/singleAppDownloadButton" />
<Button
android:id="@+id/singleAppCopyZRIFButton"
android:layout_width="0dp"
@@ -94,7 +81,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/singleAppCopyPKGButton" />
app:layout_constraintTop_toBottomOf="@+id/singleAppDownloadButton" />
<TextView
android:id="@+id/singleAppContentIdTitle"
@@ -147,23 +134,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/singleAppContentId" />
<TextView
android:id="@+id/singleAppVerTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="48dp"
android:fontFamily="sans-serif-black"
android:text="@string/version"
android:textAlignment="center"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/singleAppSize" />
<TextView
android:id="@+id/singleAppFwVerTitle"
android:layout_width="match_parent"
@@ -198,23 +168,6 @@
app:layout_constraintTop_toBottomOf="@+id/singleAppSizeTitle"
tools:text="Size" />
<TextView
android:id="@+id/singleAppVer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="48dp"
android:fontFamily="sans-serif-light"
android:textAlignment="center"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/singleAppVerTitle"
tools:text="Version" />
<TextView
android:id="@+id/singleAppFwVer"
android:layout_width="match_parent"
@@ -232,6 +185,7 @@
app:layout_constraintTop_toBottomOf="@+id/singleAppFwVerTitle"
tools:text="MinFW" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_24dp"
android:title="@string/search"
app:iconTint="?attr/colorOnPrimary"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
android:title="@string/title_activity_settings"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" />
</menu>
+1 -1
View File
@@ -7,5 +7,5 @@
<color name="colorSecondaryDark">#515151</color>
<color name="colorOnSecondary">#DFFFFFFF</color>
<color name="colorAmoled">#000000</color>
<color name="colorAccent">#F93422</color>
<color name="colorError">#F93422</color>
</resources>
+4 -5
View File
@@ -2,13 +2,12 @@
<resources>
<string name="app_name">NPS Browser</string>
<string name="title_activity_settings">设置</string>
<string name="search">搜索</string>
<string name="search">搜索…</string>
<string name="search_tooltip">搜索</string>
<string name="download">下载</string>
<string name="copy_pkg">复制PKG地址</string>
<string name="copy_zrif">复制zRIF密钥</string>
<string name="copy_zrif">复制zRIF</string>
<string name="content_id">内容ID</string>
<string name="size">大小</string>
<string name="version">版本</string>
<string name="minfw">最低固件版本</string>
<string name="single_app_icon">应用图标</string>
<string name="no_license">注意:这个内容没有提供授权</string>
@@ -17,7 +16,7 @@
<string name="download_error">下载过程中发生错误,请重试</string>
<string name="pkg_url_copied">PKG地址已复制到剪贴板</string>
<string name="zrif_copied">zRIF已复制到剪贴板</string>
<string name="downloading">下载中...</string>
<string name="downloading">下载中…</string>
<string name="content_id_copied">内容ID已复制到剪贴板</string>
<string name="appearance">外观</string>
<string name="theme">主题</string>
+4 -5
View File
@@ -2,13 +2,12 @@
<resources>
<string name="app_name">NPS Browser</string>
<string name="title_activity_settings">設置</string>
<string name="search">搜尋</string>
<string name="search">搜尋…</string>
<string name="search_tooltip">搜尋</string>
<string name="download">下載</string>
<string name="copy_pkg">複製PKG地址</string>
<string name="copy_zrif">複製zRIF金鑰</string>
<string name="copy_zrif">複製zRIF</string>
<string name="content_id">內容ID</string>
<string name="size">檔案大小</string>
<string name="version">版本</string>
<string name="minfw">最低韌體版本</string>
<string name="single_app_icon">應用程式圖標</string>
<string name="no_license">注意:這個內容沒有提供授權</string>
@@ -17,7 +16,7 @@
<string name="download_error">下載過程中發生錯誤,請重試</string>
<string name="pkg_url_copied">PKG地址已複製到剪貼板</string>
<string name="zrif_copied">zRIF已複製到剪貼板</string>
<string name="downloading">正在下載...</string>
<string name="downloading">正在下載…</string>
<string name="content_id_copied">內容ID已複製到剪貼板</string>
<string name="appearance">外觀</string>
<string name="theme">主題</string>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="app_theme_val">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="layout_type_val">
<item>0</item>
<item>1</item>
</string-array>
</resources>
+2 -1
View File
@@ -7,5 +7,6 @@
<color name="colorSecondaryDark">#888888</color>
<color name="colorOnSecondary">#111111</color>
<color name="colorAmoled">#FFFFFF</color>
<color name="colorAccent">#F93422</color>
<color name="colorError">#F93422</color>
<color name="colorAccent">#545FBF</color>
</resources>
+4 -5
View File
@@ -2,19 +2,18 @@
<resources>
<string name="app_name">NPS Browser</string>
<string name="title_activity_settings">Settings</string>
<string name="search">Search</string>
<string name="search">Search…</string>
<string name="search_tooltip">Search</string>
<string name="download">Download</string>
<string name="copy_pkg">Copy PKG URL</string>
<string name="copy_zrif">Copy zRIF Key</string>
<string name="copy_zrif">Copy zRIF</string>
<string name="content_id">Content ID</string>
<string name="size">Size</string>
<string name="version">Version</string>
<string name="minfw">Minimum Firmware Version</string>
<string name="single_app_icon">Single app icon</string>
<string name="no_license">Warning: No license was provided for this content.</string>
<string name="cart_only">CART ONLY GAME</string>
<string name="no_download_link">Warning: No download link was provided for this content.</string>
<string name="downloading">Downloading...</string>
<string name="downloading">Downloading…</string>
<string name="download_error">An error occurred while downloading. please try again</string>
<string name="pkg_url_copied">PKG URL Copied to Clipboard</string>
<string name="zrif_copied">zRIF Copied to Clipboard</string>
+9 -9
View File
@@ -1,7 +1,7 @@
<resources>
<!-- Base application theme. -->
<style name="NPSTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="NPSTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryVariant">@color/colorPrimaryDark</item>
@@ -14,7 +14,7 @@
<item name="alertDialogTheme">@style/NPSTheme.AlertDialog</item>
</style>
<style name="NPSTheme.AlertDialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert">
<style name="NPSTheme.AlertDialog" parent="Theme.Material3.DayNight.Dialog.Alert">
<item name="colorPrimary">@color/colorOnPrimary</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
@@ -30,13 +30,13 @@
<item name="windowNoTitle">true</item>
</style>
<style name="NPSTheme.Splash" parent="NPSTheme">
<item name="android:statusBarColor">#FF00A7FF</item>
<item name="android:windowBackground">@drawable/splash</item>
<style name="NPSTheme.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/colorAccent</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_nps_fg_no_shadow</item>
<item name="windowSplashScreenAnimationDuration">800</item>
<item name="postSplashScreenTheme">@style/NPSTheme</item>
</style>
<style name="NPSTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="NPSTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
-19
View File
@@ -1,19 +0,0 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/appearance">
<com.illusionware.npsbrowser.ThemePreference
android:defaultValue="2"
android:entries="@array/app_theme"
android:entryValues="@array/app_theme_val"
android:key="app_theme"
android:title="@string/theme" />
<ListPreference
android:defaultValue="0"
android:entries="@array/layout_type"
android:entryValues="@array/layout_type_val"
android:key="layout_type"
android:title="@string/layout"
app:useSimpleSummaryProvider="true"/>
</PreferenceCategory>
</PreferenceScreen>
+11 -11
View File
@@ -1,17 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
kotlin_version = '1.8.10'
}
val kotlin_version = "1.8.22"
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath("com.android.tools.build:gradle:8.0.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -21,11 +19,13 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven( url = "https://jitpack.io" )
}
}
task clean(type: Delete) {
delete rootProject.buildDir
tasks {
register("clean", Delete::class) {
delete(rootProject.buildDir)
}
}
+3 -2
View File
@@ -15,7 +15,8 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
+2 -2
View File
@@ -1,6 +1,6 @@
#Tue Oct 20 12:27:05 GMT+03:00 2020
#Wed Jun 21 06:42:34 EEST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
-2
View File
@@ -1,2 +0,0 @@
rootProject.name='NPS Browser'
include ':app'
+2
View File
@@ -0,0 +1,2 @@
rootProject.name="NPS Browser"
include("app")