refactor/feat/WIP: completely move to compose, partially finished onboarding screen

This commit is contained in:
2023-06-24 06:47:18 +03:00
parent 759f584a67
commit a99be170c0
33 changed files with 1574 additions and 908 deletions
+13 -27
View File
@@ -4,8 +4,6 @@ import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
id("kotlin-parcelize")
id("kotlin-kapt")
} }
val keystoreProperties = Properties() val keystoreProperties = Properties()
@@ -32,7 +30,6 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding = true
compose = true compose = true
aidl = false aidl = false
@@ -52,7 +49,8 @@ android {
buildTypes { buildTypes {
named("release") { named("release") {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false isMinifyEnabled = true
isShrinkResources = true
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
} }
} }
@@ -69,12 +67,6 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "17"
} }
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
} }
repositories { repositories {
@@ -83,8 +75,8 @@ repositories {
} }
dependencies { dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.5.1") implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2022.10.00")) implementation(platform("androidx.compose:compose-bom:2022.10.00"))
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
@@ -96,32 +88,26 @@ dependencies {
androidTestImplementation(composeBom) androidTestImplementation(composeBom)
// Material 3 // Material 3
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3:1.1.0")
implementation("androidx.compose.material:material-icons-extended:1.4.3")
// Android Studio Preview support // Android Studio Preview support
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.appcompat:appcompat:1.6.1") // Preferences DataStore (alternative to SharedPreferences)
implementation("androidx.core:core-ktx:1.10.1") implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0") // Compose extras library for controlling system ui and etc.
implementation("com.google.accompanist:accompanist-systemuicontroller:0.31.2-alpha")
implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.8.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.6.0")
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.recyclerview:recyclerview:1.3.0")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("com.google.android.material:material:1.9.0") implementation("com.google.android.material:material:1.9.0")
implementation("com.github.bumptech.glide:glide:4.14.2") implementation("com.github.bumptech.glide:glide:4.14.2")
annotationProcessor("com.github.bumptech.glide:compiler:4.14.2") annotationProcessor("com.github.bumptech.glide:compiler:4.14.2")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
debugImplementation("androidx.compose.ui:ui-test-manifest") debugImplementation("androidx.compose.ui:ui-test-manifest")
implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.navigation:navigation-compose:2.6.0")
} }
@@ -1,59 +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 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 (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 + (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,47 @@
package com.illusionware.npsbrowser
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.map
class Preferences(private val context: Context) {
companion object {
private val Context.dataStore by preferencesDataStore(name = "settings")
private val APP_THEME = intPreferencesKey("app_theme")
private val DATA_LAYOUT = intPreferencesKey("layout")
private val SEEN_ONBOARDING = booleanPreferencesKey("seen_onboarding")
}
val theme = context.dataStore.data.map { prefs ->
prefs[APP_THEME] ?: 2
}
val layout = context.dataStore.data.map{ prefs ->
prefs[DATA_LAYOUT] ?: 0
}
val seenOnboarding = context.dataStore.data.map{ prefs ->
prefs[SEEN_ONBOARDING] ?: false
}
suspend fun changeTheme(theme: Int) {
context.dataStore.edit { prefs ->
prefs[APP_THEME] = theme
}
}
suspend fun changeLayout(layout: Int) {
context.dataStore.edit { prefs ->
prefs[DATA_LAYOUT] = layout
}
}
suspend fun finishOnboarding() {
context.dataStore.edit { prefs ->
prefs[SEEN_ONBOARDING] = true
}
}
}
@@ -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))
}
}
})
}
}
@@ -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,58 +1,296 @@
package com.illusionware.npsbrowser.activities package com.illusionware.npsbrowser.activities
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentManager import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.illusionware.npsbrowser.Preferences
import com.illusionware.npsbrowser.R import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.fragments.mainactivity.MainActivityFragment import com.illusionware.npsbrowser.ui.theme.ColorAccent
import com.illusionware.npsbrowser.ui.theme.NPSBrowserTheme
import com.illusionware.npsbrowser.ui.theme.Typography
import com.illusionware.npsbrowser.ui.components.*
import com.illusionware.npsbrowser.ui.pages.SettingsScreen
import com.illusionware.npsbrowser.ui.pages.OnBoarding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class MainActivity : BaseActivity(), FragmentManager.OnBackStackChangedListener { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setTheme(R.style.NPSTheme) setContent {
setContentView(R.layout.activity_main) Router()
setSupportActionBar(findViewById(R.id.toolbar)) }
supportFragmentManager.addOnBackStackChangedListener(this); }
}
if (supportFragmentManager.backStackEntryCount > 0) {
showUpButton() @Composable
fun Router() {
val prefs = Preferences(LocalContext.current)
var theme by remember { runBlocking { mutableStateOf(prefs.theme.first()) } }
var seenOnboarding by remember { runBlocking { mutableStateOf(prefs.seenOnboarding.first()) } }
val navController = rememberNavController()
val darkTheme = when (theme) {
0 -> false
1 -> true
else -> isSystemInDarkTheme()
}
NPSBrowserTheme(darkTheme = darkTheme) {
Surface(
modifier = Modifier.fillMaxSize(),
color = if (seenOnboarding) MaterialTheme.colorScheme.background else ColorAccent
) {
NavHost(
navController = navController,
startDestination = if (seenOnboarding) "Home" else "Onboarding",
) {
composable(route = "Home") {
HomePage(
navigateToSettings = { navController.navigate("Settings") }
)
}
composable(route = "Settings") {
SettingsScreen(
navigationGoBack = { navController.popBackStack() },
onThemeChange = { v -> theme = v },
)
}
composable(route = "Onboarding") {
OnBoarding(
navigateToHome = {
navController.popBackStack()
seenOnboarding = true
navController.navigate("Home")
},
darkTheme = darkTheme
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview()
fun HomePage(navigateToSettings: () -> Unit = {}) {
var isSearchOpen by rememberSaveable { mutableStateOf(false)}
var searchQuery by rememberSaveable { mutableStateOf("")}
val searchBar = remember { FocusRequester() }
var layoutDialogOpen by remember { mutableStateOf(false) }
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")
}
}
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 = Icons.Filled.GridView, 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
))
}
}
)
}
) { padding ->
Column(Modifier.padding(padding)) {
NoTsvs(navigateToSettings = navigateToSettings)
}
// TODO: only show NoTsvs when there are no Tsvs loaded
if (layoutDialogOpen) {
LayoutDialog(onDismiss = { layoutDialogOpen = false })
}
}
}
@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 LayoutDialog(onDismiss: () -> Unit = {}) {
val prefs = Preferences(LocalContext.current)
val layout = prefs.layout.collectAsState(initial = "")
NPSAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.layout),
buttons = {
TextButton(onClick = onDismiss) {
Text(text = "Cancel")
}
}
) {
Column {
NPSRadioButton(
text = "List",
selected = layout.value == 0,
onClick = { CoroutineScope(Dispatchers.IO).launch { prefs.changeLayout(0) } },
)
NPSRadioButton(
text = "Grid",
selected = layout.value == 1,
onClick = { CoroutineScope(Dispatchers.IO).launch { prefs.changeLayout(1) } },
)
} }
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MainActivityFragment())
.commitNow()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
supportFragmentManager.popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onBackStackChanged() {
// enable Up button only if there are entries on the backstack
if (supportFragmentManager.backStackEntryCount < 1) {
hideUpButton()
} else {
showUpButton()
}
}
private fun showUpButton() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
private fun hideUpButton() {
supportActionBar!!.setDisplayHomeAsUpEnabled(false)
} }
} }
@@ -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
}
}
@@ -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,191 +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
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.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.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()
}
}
// long click listener for download button
view?.findViewById<Button>(R.id.singleAppDownloadButton)?.setOnLongClickListener {
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()
true
}
// click listener for the copyzrifbutton 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,283 +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 sha256 = entry["SHA256"]
val minFW = entry["Required FW"]
val app = AppData(
id, region, name, link ?: "MISSING", license ?: "MISSING",
contentID ?: "MISSING", lastDateTime ?: "MISSING",
fileSize, 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,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,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,474 @@
package com.illusionware.npsbrowser.ui.pages
import android.annotation.SuppressLint
import android.app.Activity
import android.content.pm.ActivityInfo
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
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.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.defaultMinSize
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.offset
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.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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonElevation
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Surface
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
const val PAGE_COUNT = 3;
@OptIn(ExperimentalFoundationApi::class)
@SuppressLint("SourceLockedOrientationActivity")
@Composable
fun OnBoarding(navigateToHome: () -> Unit = {}, darkTheme: Boolean = false) {
val systemUiController = rememberSystemUiController()
val pagerState = rememberPagerState()
val scope = rememberCoroutineScope()
val context = LocalContext.current
val darkIcons = when (pagerState.currentPage) {
0 -> false
else -> true
}
DisposableEffect(systemUiController, darkIcons) {
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) } }
)
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
)
}
}
}
@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
@Preview
fun OnBoardingSetupTSVs(
page: Int = 1,
incrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {}
) {
Scaffold() { padding ->
Column(
Modifier
.padding(padding)
.padding(horizontal = 16.dp, vertical = 32.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Setup 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),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TSVGroup(title = "PS Vita") {
TSVItem("Games")
TSVItem("Themes")
TSVItem("DLC")
}
TSVGroup(title = "PSP") {
TSVItem("Games")
TSVItem("DLC")
Box(modifier = Modifier.width(110.dp))
}
TSVGroup(title = "PS3") {
TSVItem("Games")
TSVItem("DLC")
Box(modifier = Modifier.width(110.dp))
}
TSVGroup(title = "PSX") {
TSVItem("Games")
}
TSVGroup(title = "PSM") {
TSVItem("Games")
}
}
OnBoardingBottomButtons(
page = page,
incrementPage = incrementPage,
skipToEnd = skipToEnd,
)
}
}
}
}
@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 TSVItem(title: String) {
var hasSelected by rememberSaveable { mutableStateOf(false) }
val filePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
) { fileUri ->
if (fileUri != null) {
Log.d("TSVItem", "fileUri: $fileUri")
hasSelected = true
}
}
FilledTonalButton(
onClick = {
filePicker.launch("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 OnBoardingBottomButtons(
page: Int,
incrementPage: () -> Unit = {},
decrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {},
navigateToHome: () -> Unit = {},
) {
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f),
horizontalArrangement = Arrangement.SpaceBetween,
) {
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 == 2) {
Button(onClick = navigateToHome, contentPadding = PaddingValues(horizontal = 18.dp)) {
Text(text = "Finish")
}
} else {
Button(
onClick = incrementPage,
contentPadding = PaddingValues(horizontal = 18.dp)
) {
Text(text = "Next")
}
}
}
}
}
@Composable
@Preview
fun OnBoardingSetupSettings(
page: Int = 2,
incrementPage: () -> Unit = {},
decrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {},
navigateToHome: () -> Unit = {},
) {
Scaffold() { padding ->
Column(Modifier.padding(padding)) {
Text(text = "Configure App")
OnBoardingBottomButtons(
page = page,
incrementPage = incrementPage,
decrementPage = decrementPage,
skipToEnd = skipToEnd,
navigateToHome = navigateToHome
)
}
}
}
@@ -0,0 +1,258 @@
package com.illusionware.npsbrowser.ui.pages
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.fillMaxWidth
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.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.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.collectAsState
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.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import com.illusionware.npsbrowser.Preferences
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
@Preview
fun SettingsScreen(navigationGoBack: () -> Unit = {}, onThemeChange: (v: Int) -> Unit = {}) {
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)
}
},
) { padding ->
Column(modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
) {
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.padding(vertical = 16.dp)) {
Appearance(onThemeChange)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header("Updates")
Spacer(modifier = Modifier.size(2.dp))
DialogSetting(title = "HMAC Key", value = "None", icon = Icons.Outlined.Key)
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header("Downloads")
Spacer(modifier = Modifier.size(2.dp))
ToggleSetting(title = "Unpack in Download Directory")
DialogSetting(title = "Download Directory", value = "sdcard/Downloads", icon = Icons.Outlined.FileDownload)
DialogSetting(title = "Unpack Directory", value = "sdcard/unpack", icon = Icons.Outlined.Unarchive)
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header("Pkg2zip")
Spacer(modifier = Modifier.size(2.dp))
ToggleSetting(title = "Automatically decrypt downloaded content")
DialogSetting(title = "Decryption params", value = "-x {pkgFile} \"{zRifKey}\"", icon = painterResource(
id = R.drawable.lock_open
))
}
}
}
}
}
@Composable
fun SettingGroup(title: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header(title)
Spacer(modifier = Modifier.size(2.dp))
content()
}
}
val Themes = hashMapOf(
0 to "Light",
1 to "Dark",
2 to "Use System Theme"
)
@Composable
fun Appearance(onThemeChange: (v: Int) -> Unit) {
val prefs = Preferences(LocalContext.current)
val theme = prefs.theme.collectAsState(initial = 0)
var themeOpenDialog by remember { mutableStateOf(false) }
SettingGroup(title = stringResource(id = R.string.appearance)) {
DialogSetting(
title = stringResource(id = R.string.theme),
value = Themes[theme.value] ?: "None",
icon = Icons.Outlined.Palette,
onClick = { themeOpenDialog = true }
)
}
if (themeOpenDialog) {
ThemeDialog(
onDismiss = { themeOpenDialog = false },
onThemeChange = onThemeChange
)
}
}
@Composable
fun ThemeDialog(onDismiss: () -> Unit, onThemeChange: (v: Int) -> Unit) {
val prefs = Preferences(LocalContext.current)
val theme = prefs.theme.collectAsState(initial = "")
NPSAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.theme),
buttons = {
TextButton(onClick = onDismiss) {
Text(text = "Cancel")
}
},
modifier = Modifier.fillMaxWidth()
) {
Column {
NPSRadioButton(
text = "Light",
onClick = { CoroutineScope(Dispatchers.IO).launch {
onThemeChange(0)
prefs.changeTheme(0) }
},
selected = theme.value == 0,
)
NPSRadioButton(
text = "Dark",
onClick = { CoroutineScope(Dispatchers.IO).launch {
onThemeChange(1)
prefs.changeTheme(1) }
},
selected = theme.value == 1,
)
NPSRadioButton(
text = "Use System Theme",
onClick = { CoroutineScope(Dispatchers.IO).launch {
onThemeChange(2)
prefs.changeTheme(2) }
},
selected = theme.value == 2,
)
}
}
}
@Composable
fun Header(title: String) {
Row(Modifier.padding(start = 56.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.primary)
}
}
@Composable
// TODO: remove the default value for onClick
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)
) {
Icon(painter = icon, contentDescription = "TODO:", modifier = Modifier.width(56.dp))
Column {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(text = value, color = ColorSecondaryLight)
}
}
}
}
@Composable
// TODO: remove the default value for onClick
fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () -> Unit = {}) {
Box(
Modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(imageVector = icon, contentDescription = "TODO:", modifier = Modifier.width(56.dp))
Column {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(text = value, color = ColorSecondaryLight)
}
}
}
}
@Composable
fun ToggleSetting(title: String, icon: ImageVector? = null) {
Box(
modifier = Modifier.clickable { /* TODO: */ },
) {
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 = "TODO:", 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 = true, onCheckedChange = { /* TODO: */ })
}
}
}
@@ -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()
// 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
)
*/
)
+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>
@@ -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>
+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>
+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>
+3 -2
View File
@@ -2,7 +2,8 @@
<resources> <resources>
<string name="app_name">NPS Browser</string> <string name="app_name">NPS Browser</string>
<string name="title_activity_settings">设置</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="download">下载</string>
<string name="copy_zrif">复制zRIF</string> <string name="copy_zrif">复制zRIF</string>
<string name="content_id">内容ID</string> <string name="content_id">内容ID</string>
@@ -15,7 +16,7 @@
<string name="download_error">下载过程中发生错误,请重试</string> <string name="download_error">下载过程中发生错误,请重试</string>
<string name="pkg_url_copied">PKG地址已复制到剪贴板</string> <string name="pkg_url_copied">PKG地址已复制到剪贴板</string>
<string name="zrif_copied">zRIF已复制到剪贴板</string> <string name="zrif_copied">zRIF已复制到剪贴板</string>
<string name="downloading">下载中...</string> <string name="downloading">下载中…</string>
<string name="content_id_copied">内容ID已复制到剪贴板</string> <string name="content_id_copied">内容ID已复制到剪贴板</string>
<string name="appearance">外观</string> <string name="appearance">外观</string>
<string name="theme">主题</string> <string name="theme">主题</string>
+3 -2
View File
@@ -2,7 +2,8 @@
<resources> <resources>
<string name="app_name">NPS Browser</string> <string name="app_name">NPS Browser</string>
<string name="title_activity_settings">設置</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="download">下載</string>
<string name="copy_zrif">複製zRIF</string> <string name="copy_zrif">複製zRIF</string>
<string name="content_id">內容ID</string> <string name="content_id">內容ID</string>
@@ -15,7 +16,7 @@
<string name="download_error">下載過程中發生錯誤,請重試</string> <string name="download_error">下載過程中發生錯誤,請重試</string>
<string name="pkg_url_copied">PKG地址已複製到剪貼板</string> <string name="pkg_url_copied">PKG地址已複製到剪貼板</string>
<string name="zrif_copied">zRIF已複製到剪貼板</string> <string name="zrif_copied">zRIF已複製到剪貼板</string>
<string name="downloading">正在下載...</string> <string name="downloading">正在下載…</string>
<string name="content_id_copied">內容ID已複製到剪貼板</string> <string name="content_id_copied">內容ID已複製到剪貼板</string>
<string name="appearance">外觀</string> <string name="appearance">外觀</string>
<string name="theme">主題</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>
+3 -3
View File
@@ -2,7 +2,8 @@
<resources> <resources>
<string name="app_name">NPS Browser</string> <string name="app_name">NPS Browser</string>
<string name="title_activity_settings">Settings</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="download">Download</string>
<string name="copy_zrif">Copy zRIF</string> <string name="copy_zrif">Copy zRIF</string>
<string name="content_id">Content ID</string> <string name="content_id">Content ID</string>
@@ -12,7 +13,7 @@
<string name="no_license">Warning: No license was provided for this content.</string> <string name="no_license">Warning: No license was provided for this content.</string>
<string name="cart_only">CART ONLY GAME</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="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="download_error">An error occurred while downloading. please try again</string>
<string name="pkg_url_copied">PKG URL Copied to Clipboard</string> <string name="pkg_url_copied">PKG URL Copied to Clipboard</string>
<string name="zrif_copied">zRIF Copied to Clipboard</string> <string name="zrif_copied">zRIF Copied to Clipboard</string>
@@ -31,5 +32,4 @@
<item>List</item> <item>List</item>
<item>Grid</item> <item>Grid</item>
</string-array> </string-array>
<string name="title_activity_thing">thing</string>
</resources> </resources>
+3 -7
View File
@@ -1,7 +1,7 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="NPSTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="NPSTheme" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryVariant">@color/colorPrimaryDark</item> <item name="colorPrimaryVariant">@color/colorPrimaryDark</item>
@@ -14,7 +14,7 @@
<item name="alertDialogTheme">@style/NPSTheme.AlertDialog</item> <item name="alertDialogTheme">@style/NPSTheme.AlertDialog</item>
</style> </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="colorPrimary">@color/colorOnPrimary</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
</style> </style>
@@ -34,13 +34,9 @@
<item name="windowSplashScreenBackground">@color/colorAccent</item> <item name="windowSplashScreenBackground">@color/colorAccent</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_nps_fg_no_shadow</item> <item name="windowSplashScreenAnimatedIcon">@drawable/ic_nps_fg_no_shadow</item>
<item name="windowSplashScreenAnimationDuration">500</item> <item name="windowSplashScreenAnimationDuration">800</item>
<item name="postSplashScreenTheme">@style/NPSTheme</item> <item name="postSplashScreenTheme">@style/NPSTheme</item>
</style> </style>
<style name="NPSTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="NPSTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources> </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>
-2
View File
@@ -6,7 +6,6 @@ buildscript {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
jcenter()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:8.0.2") classpath("com.android.tools.build:gradle:8.0.2")
@@ -21,7 +20,6 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
jcenter()
} }
} }
-2
View File
@@ -15,8 +15,6 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true 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 for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true android.defaults.buildfeatures.buildconfig=true
+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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip